@roomi-fields/notebooklm-mcp 1.3.5 → 1.5.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.
- package/README.md +93 -658
- package/dist/accounts/account-manager.d.ts +163 -0
- package/dist/accounts/account-manager.d.ts.map +1 -0
- package/dist/accounts/account-manager.js +614 -0
- package/dist/accounts/account-manager.js.map +1 -0
- package/dist/accounts/auto-login-manager.d.ts +62 -0
- package/dist/accounts/auto-login-manager.d.ts.map +1 -0
- package/dist/accounts/auto-login-manager.js +537 -0
- package/dist/accounts/auto-login-manager.js.map +1 -0
- package/dist/accounts/crypto.d.ts +45 -0
- package/dist/accounts/crypto.d.ts.map +1 -0
- package/dist/accounts/crypto.js +138 -0
- package/dist/accounts/crypto.js.map +1 -0
- package/dist/accounts/index.d.ts +14 -0
- package/dist/accounts/index.d.ts.map +1 -0
- package/dist/accounts/index.js +14 -0
- package/dist/accounts/index.js.map +1 -0
- package/dist/accounts/types.d.ts +103 -0
- package/dist/accounts/types.d.ts.map +1 -0
- package/dist/accounts/types.js +7 -0
- package/dist/accounts/types.js.map +1 -0
- package/dist/auth/auth-manager.d.ts +9 -2
- package/dist/auth/auth-manager.d.ts.map +1 -1
- package/dist/auth/auth-manager.js +60 -6
- package/dist/auth/auth-manager.js.map +1 -1
- package/dist/auto-discovery/auto-discovery.d.ts.map +1 -1
- package/dist/auto-discovery/auto-discovery.js +2 -1
- package/dist/auto-discovery/auto-discovery.js.map +1 -1
- package/dist/cli/accounts.d.ts +13 -0
- package/dist/cli/accounts.d.ts.map +1 -0
- package/dist/cli/accounts.js +195 -0
- package/dist/cli/accounts.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/content/content-generator.d.ts +153 -0
- package/dist/content/content-generator.d.ts.map +1 -0
- package/dist/content/content-generator.js +637 -0
- package/dist/content/content-generator.js.map +1 -0
- package/dist/content/content-manager.d.ts +364 -0
- package/dist/content/content-manager.d.ts.map +1 -0
- package/dist/content/content-manager.js +3846 -0
- package/dist/content/content-manager.js.map +1 -0
- package/dist/content/content-templates.d.ts +183 -0
- package/dist/content/content-templates.d.ts.map +1 -0
- package/dist/content/content-templates.js +719 -0
- package/dist/content/content-templates.js.map +1 -0
- package/dist/content/index.d.ts +14 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +14 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/types.d.ts +285 -0
- package/dist/content/types.d.ts.map +1 -0
- package/dist/content/types.js +10 -0
- package/dist/content/types.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/http-wrapper.d.ts +7 -0
- package/dist/http-wrapper.d.ts.map +1 -1
- package/dist/http-wrapper.js +449 -29
- package/dist/http-wrapper.js.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/library/notebook-library.d.ts +4 -0
- package/dist/library/notebook-library.d.ts.map +1 -1
- package/dist/library/notebook-library.js +20 -3
- package/dist/library/notebook-library.js.map +1 -1
- package/dist/session/browser-session.d.ts +35 -8
- package/dist/session/browser-session.d.ts.map +1 -1
- package/dist/session/browser-session.js +242 -28
- package/dist/session/browser-session.js.map +1 -1
- package/dist/session/session-manager.d.ts +6 -0
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +46 -14
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/shared-context-manager.d.ts +3 -3
- package/dist/session/shared-context-manager.d.ts.map +1 -1
- package/dist/session/shared-context-manager.js +8 -7
- package/dist/session/shared-context-manager.js.map +1 -1
- package/dist/stdio-http-proxy.d.ts +24 -0
- package/dist/stdio-http-proxy.d.ts.map +1 -0
- package/dist/stdio-http-proxy.js +592 -0
- package/dist/stdio-http-proxy.js.map +1 -0
- package/dist/tools/index.d.ts +106 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1028 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/types.d.ts +81 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/citation-extractor.d.ts +66 -0
- package/dist/utils/citation-extractor.d.ts.map +1 -0
- package/dist/utils/citation-extractor.js +492 -0
- package/dist/utils/citation-extractor.js.map +1 -0
- package/dist/utils/page-utils.d.ts +8 -0
- package/dist/utils/page-utils.d.ts.map +1 -1
- package/dist/utils/page-utils.js +112 -8
- package/dist/utils/page-utils.js.map +1 -1
- package/docs/ARCHITECTURE_MIGRATION_STUDY.md +894 -0
- package/docs/CHROME_PROFILE_LIMITATION.md +15 -1
- package/docs/MULTI_ACCOUNT_SYSTEM.md +304 -0
- package/package.json +10 -10
- package/dist/__tests__/cleanup-manager.test.d.ts +0 -2
- package/dist/__tests__/cleanup-manager.test.d.ts.map +0 -1
- package/dist/__tests__/cleanup-manager.test.js +0 -341
- package/dist/__tests__/cleanup-manager.test.js.map +0 -1
- package/dist/__tests__/config-parsing.test.d.ts +0 -2
- package/dist/__tests__/config-parsing.test.d.ts.map +0 -1
- package/dist/__tests__/config-parsing.test.js +0 -338
- package/dist/__tests__/config-parsing.test.js.map +0 -1
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.js +0 -267
- package/dist/__tests__/config.test.js.map +0 -1
- package/dist/__tests__/errors.test.d.ts +0 -2
- package/dist/__tests__/errors.test.d.ts.map +0 -1
- package/dist/__tests__/errors.test.js +0 -166
- package/dist/__tests__/errors.test.js.map +0 -1
- package/dist/__tests__/logger.test.d.ts +0 -2
- package/dist/__tests__/logger.test.d.ts.map +0 -1
- package/dist/__tests__/logger.test.js +0 -324
- package/dist/__tests__/logger.test.js.map +0 -1
- package/dist/__tests__/page-utils.test.d.ts +0 -2
- package/dist/__tests__/page-utils.test.d.ts.map +0 -1
- package/dist/__tests__/page-utils.test.js +0 -349
- package/dist/__tests__/page-utils.test.js.map +0 -1
- package/dist/__tests__/setup-verification.test.d.ts +0 -2
- package/dist/__tests__/setup-verification.test.d.ts.map +0 -1
- package/dist/__tests__/setup-verification.test.js +0 -15
- package/dist/__tests__/setup-verification.test.js.map +0 -1
- package/dist/__tests__/stealth-utils.test.d.ts +0 -2
- package/dist/__tests__/stealth-utils.test.d.ts.map +0 -1
- package/dist/__tests__/stealth-utils.test.js +0 -413
- package/dist/__tests__/stealth-utils.test.js.map +0 -1
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.js +0 -461
- package/dist/__tests__/types.test.js.map +0 -1
package/dist/tools/index.js
CHANGED
|
@@ -8,13 +8,22 @@
|
|
|
8
8
|
* - reset_session: Reset session chat history
|
|
9
9
|
* - get_health: Server health check
|
|
10
10
|
* - setup_auth: Interactive authentication setup
|
|
11
|
+
* - add_source: Add document/source to notebook
|
|
12
|
+
* - generate_content: Generate audio, briefing, study guide, etc.
|
|
13
|
+
* - list_content: List sources and generated content
|
|
11
14
|
*
|
|
12
15
|
* Based on the Python implementation from tools/*.py
|
|
13
16
|
*/
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { getAccountManager } from '../accounts/account-manager.js';
|
|
20
|
+
import { AutoLoginManager } from '../accounts/auto-login-manager.js';
|
|
14
21
|
import { CONFIG, applyBrowserOptions } from '../config.js';
|
|
15
22
|
import { log } from '../utils/logger.js';
|
|
16
23
|
import { RateLimitError } from '../errors.js';
|
|
17
24
|
import { CleanupManager } from '../utils/cleanup-manager.js';
|
|
25
|
+
import { randomDelay } from '../utils/stealth-utils.js';
|
|
26
|
+
import { ContentManager } from '../content/content-manager.js';
|
|
18
27
|
/**
|
|
19
28
|
* Build dynamic tool description for ask_question based on active notebook or library
|
|
20
29
|
*/
|
|
@@ -153,6 +162,17 @@ export function buildToolDefinitions(library) {
|
|
|
153
162
|
description: 'Show browser window for debugging (simple version). ' +
|
|
154
163
|
'For advanced control (typing speed, stealth, etc.), use browser_options instead.',
|
|
155
164
|
},
|
|
165
|
+
source_format: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
enum: ['none', 'inline', 'footnotes', 'json', 'expanded'],
|
|
168
|
+
description: 'Format for source citation extraction (default: none). Options:\n' +
|
|
169
|
+
'- none: No source extraction (fastest)\n' +
|
|
170
|
+
'- inline: Insert source text inline: "text [1: source excerpt]"\n' +
|
|
171
|
+
'- footnotes: Append sources at the end as footnotes\n' +
|
|
172
|
+
'- json: Return sources as separate object in response\n' +
|
|
173
|
+
'- expanded: Replace [1] with full quoted source text\n\n' +
|
|
174
|
+
'Note: Source extraction adds ~1-2 seconds but does NOT consume additional NotebookLM quota.',
|
|
175
|
+
},
|
|
156
176
|
browser_options: {
|
|
157
177
|
type: 'object',
|
|
158
178
|
description: 'Optional browser behavior settings. Claude can control everything: ' +
|
|
@@ -726,6 +746,284 @@ User: "Yes" → call remove_notebook`,
|
|
|
726
746
|
required: ['confirm'],
|
|
727
747
|
},
|
|
728
748
|
},
|
|
749
|
+
// ========================================================================
|
|
750
|
+
// Content Management Tools
|
|
751
|
+
// ========================================================================
|
|
752
|
+
{
|
|
753
|
+
name: 'add_source',
|
|
754
|
+
description: 'Add a source (document, URL, text, YouTube video) to the current NotebookLM notebook.\n\n' +
|
|
755
|
+
'Supported source types:\n' +
|
|
756
|
+
'- file: Upload a local file (PDF, DOCX, TXT, etc.)\n' +
|
|
757
|
+
'- url: Add a web page URL\n' +
|
|
758
|
+
'- text: Paste text content directly\n' +
|
|
759
|
+
'- youtube: Add a YouTube video URL\n' +
|
|
760
|
+
'- google_drive: Add a Google Drive document link\n\n' +
|
|
761
|
+
'The source will be processed and indexed for use in conversations.',
|
|
762
|
+
inputSchema: {
|
|
763
|
+
type: 'object',
|
|
764
|
+
properties: {
|
|
765
|
+
source_type: {
|
|
766
|
+
type: 'string',
|
|
767
|
+
enum: ['file', 'url', 'text', 'youtube', 'google_drive'],
|
|
768
|
+
description: 'Type of source to add',
|
|
769
|
+
},
|
|
770
|
+
file_path: {
|
|
771
|
+
type: 'string',
|
|
772
|
+
description: 'Local file path (required for source_type="file")',
|
|
773
|
+
},
|
|
774
|
+
url: {
|
|
775
|
+
type: 'string',
|
|
776
|
+
description: 'URL (required for source_type="url", "youtube", "google_drive")',
|
|
777
|
+
},
|
|
778
|
+
text: {
|
|
779
|
+
type: 'string',
|
|
780
|
+
description: 'Text content (required for source_type="text")',
|
|
781
|
+
},
|
|
782
|
+
title: {
|
|
783
|
+
type: 'string',
|
|
784
|
+
description: 'Optional title/name for the source',
|
|
785
|
+
},
|
|
786
|
+
notebook_url: {
|
|
787
|
+
type: 'string',
|
|
788
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
789
|
+
},
|
|
790
|
+
session_id: {
|
|
791
|
+
type: 'string',
|
|
792
|
+
description: 'Session ID to reuse an existing session',
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
required: ['source_type'],
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: 'delete_source',
|
|
800
|
+
description: 'Delete a source from the current NotebookLM notebook.\n\n' +
|
|
801
|
+
'You can identify the source to delete by either:\n' +
|
|
802
|
+
'- source_id: The unique identifier of the source\n' +
|
|
803
|
+
'- source_name: The name/title of the source (partial match supported)\n\n' +
|
|
804
|
+
'Use list_content first to see available sources and their IDs/names.\n\n' +
|
|
805
|
+
'WARNING: This action is irreversible. The source will be permanently removed from the notebook.',
|
|
806
|
+
inputSchema: {
|
|
807
|
+
type: 'object',
|
|
808
|
+
properties: {
|
|
809
|
+
source_id: {
|
|
810
|
+
type: 'string',
|
|
811
|
+
description: 'The unique ID of the source to delete',
|
|
812
|
+
},
|
|
813
|
+
source_name: {
|
|
814
|
+
type: 'string',
|
|
815
|
+
description: 'The name/title of the source to delete (partial match supported)',
|
|
816
|
+
},
|
|
817
|
+
notebook_url: {
|
|
818
|
+
type: 'string',
|
|
819
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
820
|
+
},
|
|
821
|
+
session_id: {
|
|
822
|
+
type: 'string',
|
|
823
|
+
description: 'Session ID to reuse an existing session',
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
name: 'generate_content',
|
|
830
|
+
description: 'Generate content from your NotebookLM sources.\n\n' +
|
|
831
|
+
'Supported content types:\n' +
|
|
832
|
+
'- audio_overview: Audio podcast/overview (Deep Dive conversation with two AI hosts)\n' +
|
|
833
|
+
'- video: Video summary that visually explains main topics (brief or explainer format)\n' +
|
|
834
|
+
'- presentation: Slides/presentation with AI-generated content and images\n' +
|
|
835
|
+
'- report: Briefing document (2,000-3,000 words) summarizing key findings, exportable as PDF/DOCX\n' +
|
|
836
|
+
'- infographic: Visual infographic in horizontal (16:9) or vertical (9:16) format\n' +
|
|
837
|
+
'- data_table: Structured table organizing key information (exportable as CSV/Excel)\n\n' +
|
|
838
|
+
'Language support: All content types support 80+ languages via the language parameter.\n\n' +
|
|
839
|
+
'Video styles: Video content supports 6 visual styles via the video_style parameter:\n' +
|
|
840
|
+
'classroom, documentary, animated, corporate, cinematic, minimalist.\n\n' +
|
|
841
|
+
'These content types use real NotebookLM Studio UI buttons or the generic ContentGenerator ' +
|
|
842
|
+
'architecture that navigates the Studio panel and falls back to chat-based generation.\n\n' +
|
|
843
|
+
'NOTE: Other content types (faq, study_guide, timeline, table_of_contents) ' +
|
|
844
|
+
'are NOT currently implemented. For document-style content, use the ask_question tool.',
|
|
845
|
+
inputSchema: {
|
|
846
|
+
type: 'object',
|
|
847
|
+
properties: {
|
|
848
|
+
content_type: {
|
|
849
|
+
type: 'string',
|
|
850
|
+
enum: [
|
|
851
|
+
'audio_overview',
|
|
852
|
+
'video',
|
|
853
|
+
'presentation',
|
|
854
|
+
'report',
|
|
855
|
+
'infographic',
|
|
856
|
+
'data_table',
|
|
857
|
+
],
|
|
858
|
+
description: 'Type of content to generate: audio_overview (podcast), video (brief or explainer), presentation (slides), report (briefing doc 2,000-3,000 words, PDF/DOCX export), infographic (horizontal 16:9 or vertical 9:16), or data_table (CSV/Excel export)',
|
|
859
|
+
},
|
|
860
|
+
custom_instructions: {
|
|
861
|
+
type: 'string',
|
|
862
|
+
description: 'Optional instructions to customize the generated content',
|
|
863
|
+
},
|
|
864
|
+
language: {
|
|
865
|
+
type: 'string',
|
|
866
|
+
description: 'Language for the generated content (e.g., "French", "Spanish", "Japanese"). NotebookLM supports 80+ languages.',
|
|
867
|
+
},
|
|
868
|
+
video_style: {
|
|
869
|
+
type: 'string',
|
|
870
|
+
enum: ['classroom', 'documentary', 'animated', 'corporate', 'cinematic', 'minimalist'],
|
|
871
|
+
description: 'Visual style for video content (only valid for content_type="video"). Powered by Nano Banana AI.',
|
|
872
|
+
},
|
|
873
|
+
notebook_url: {
|
|
874
|
+
type: 'string',
|
|
875
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
876
|
+
},
|
|
877
|
+
session_id: {
|
|
878
|
+
type: 'string',
|
|
879
|
+
description: 'Session ID to reuse an existing session',
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
required: ['content_type'],
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: 'list_content',
|
|
887
|
+
description: 'List all sources and generated content in the current notebook.\n\n' +
|
|
888
|
+
'Returns:\n' +
|
|
889
|
+
'- Sources: Documents, URLs, and other uploaded materials\n' +
|
|
890
|
+
'- Generated content: Audio overviews',
|
|
891
|
+
inputSchema: {
|
|
892
|
+
type: 'object',
|
|
893
|
+
properties: {
|
|
894
|
+
notebook_url: {
|
|
895
|
+
type: 'string',
|
|
896
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
897
|
+
},
|
|
898
|
+
session_id: {
|
|
899
|
+
type: 'string',
|
|
900
|
+
description: 'Session ID to reuse an existing session',
|
|
901
|
+
},
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: 'download_content',
|
|
907
|
+
description: 'Download or export generated content from NotebookLM.\n\n' +
|
|
908
|
+
'Supported content types:\n' +
|
|
909
|
+
'- audio_overview: Downloads as audio file (MP3)\n' +
|
|
910
|
+
'- video: Downloads as video file\n' +
|
|
911
|
+
'- infographic: Downloads as image file\n' +
|
|
912
|
+
'- presentation: Exports to Google Slides (returns URL)\n' +
|
|
913
|
+
'- data_table: Exports to Google Sheets (returns URL)\n\n' +
|
|
914
|
+
'Note: Report content is text-based and returned in the generation response.',
|
|
915
|
+
inputSchema: {
|
|
916
|
+
type: 'object',
|
|
917
|
+
properties: {
|
|
918
|
+
content_type: {
|
|
919
|
+
type: 'string',
|
|
920
|
+
enum: ['audio_overview', 'video', 'infographic', 'presentation', 'data_table'],
|
|
921
|
+
description: 'Type of content to download/export',
|
|
922
|
+
},
|
|
923
|
+
output_path: {
|
|
924
|
+
type: 'string',
|
|
925
|
+
description: 'Optional local path to save the file (for audio, video, infographic)',
|
|
926
|
+
},
|
|
927
|
+
notebook_url: {
|
|
928
|
+
type: 'string',
|
|
929
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
930
|
+
},
|
|
931
|
+
session_id: {
|
|
932
|
+
type: 'string',
|
|
933
|
+
description: 'Session ID to reuse an existing session',
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
required: ['content_type'],
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: 'create_note',
|
|
941
|
+
description: 'Create a note in the NotebookLM Studio panel.\n\n' +
|
|
942
|
+
'Notes are user-created annotations that appear in your notebook. ' +
|
|
943
|
+
'Use them to save research findings, summaries, key insights, or any ' +
|
|
944
|
+
'custom content you want to keep alongside your sources.\n\n' +
|
|
945
|
+
'Notes support markdown formatting for rich text content.',
|
|
946
|
+
inputSchema: {
|
|
947
|
+
type: 'object',
|
|
948
|
+
properties: {
|
|
949
|
+
title: {
|
|
950
|
+
type: 'string',
|
|
951
|
+
description: 'Title of the note (required)',
|
|
952
|
+
},
|
|
953
|
+
content: {
|
|
954
|
+
type: 'string',
|
|
955
|
+
description: 'Content/body of the note. Supports markdown formatting.',
|
|
956
|
+
},
|
|
957
|
+
notebook_url: {
|
|
958
|
+
type: 'string',
|
|
959
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
960
|
+
},
|
|
961
|
+
session_id: {
|
|
962
|
+
type: 'string',
|
|
963
|
+
description: 'Session ID to reuse an existing session',
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
required: ['title', 'content'],
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
name: 'save_chat_to_note',
|
|
971
|
+
description: 'Save the current NotebookLM chat/discussion to a note.\n\n' +
|
|
972
|
+
'This tool extracts all messages from the current conversation (both user questions ' +
|
|
973
|
+
'and NotebookLM AI responses) and saves them as a formatted note in the Studio panel.\n\n' +
|
|
974
|
+
'Use this to:\n' +
|
|
975
|
+
'- Preserve important research conversations\n' +
|
|
976
|
+
'- Create a summary of your discussion with NotebookLM\n' +
|
|
977
|
+
'- Save chat history before starting a new topic\n\n' +
|
|
978
|
+
'The note will include timestamps and message attribution (User/NotebookLM).',
|
|
979
|
+
inputSchema: {
|
|
980
|
+
type: 'object',
|
|
981
|
+
properties: {
|
|
982
|
+
title: {
|
|
983
|
+
type: 'string',
|
|
984
|
+
description: 'Custom title for the note (default: "Chat Summary")',
|
|
985
|
+
},
|
|
986
|
+
notebook_url: {
|
|
987
|
+
type: 'string',
|
|
988
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
989
|
+
},
|
|
990
|
+
session_id: {
|
|
991
|
+
type: 'string',
|
|
992
|
+
description: 'Session ID to reuse an existing session',
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
name: 'convert_note_to_source',
|
|
999
|
+
description: 'Convert a note to a source document in NotebookLM.\n\n' +
|
|
1000
|
+
'This feature allows you to convert an existing note into a source, ' +
|
|
1001
|
+
'making the note content available for RAG queries and research.\n\n' +
|
|
1002
|
+
'The method:\n' +
|
|
1003
|
+
'1. Finds the note by title in the Studio panel\n' +
|
|
1004
|
+
'2. Attempts to use NotebookLM\'s native "Convert to source" feature if available\n' +
|
|
1005
|
+
'3. Falls back to extracting note content and creating a text source if not\n\n' +
|
|
1006
|
+
"Use this when you want your note content to be included in NotebookLM's " +
|
|
1007
|
+
'knowledge base for answering questions.',
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
type: 'object',
|
|
1010
|
+
properties: {
|
|
1011
|
+
note_title: {
|
|
1012
|
+
type: 'string',
|
|
1013
|
+
description: 'Title of the note to convert (required)',
|
|
1014
|
+
},
|
|
1015
|
+
notebook_url: {
|
|
1016
|
+
type: 'string',
|
|
1017
|
+
description: 'Notebook URL. If not provided, uses the active notebook.',
|
|
1018
|
+
},
|
|
1019
|
+
session_id: {
|
|
1020
|
+
type: 'string',
|
|
1021
|
+
description: 'Session ID to reuse an existing session',
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
required: ['note_title'],
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
729
1027
|
];
|
|
730
1028
|
}
|
|
731
1029
|
/**
|
|
@@ -744,7 +1042,7 @@ export class ToolHandlers {
|
|
|
744
1042
|
* Handle ask_question tool
|
|
745
1043
|
*/
|
|
746
1044
|
async handleAskQuestion(args, sendProgress) {
|
|
747
|
-
const { question, session_id, notebook_id, notebook_url, show_browser, browser_options } = args;
|
|
1045
|
+
const { question, session_id, notebook_id, notebook_url, show_browser, source_format = 'none', browser_options, } = args;
|
|
748
1046
|
log.info(`🔧 [TOOL] ask_question called`);
|
|
749
1047
|
log.info(` Question: "${question.substring(0, 100)}..."`);
|
|
750
1048
|
if (session_id) {
|
|
@@ -756,6 +1054,9 @@ export class ToolHandlers {
|
|
|
756
1054
|
if (notebook_url) {
|
|
757
1055
|
log.info(` Notebook URL: ${notebook_url}`);
|
|
758
1056
|
}
|
|
1057
|
+
if (source_format !== 'none') {
|
|
1058
|
+
log.info(` Source format: ${source_format}`);
|
|
1059
|
+
}
|
|
759
1060
|
try {
|
|
760
1061
|
// Resolve notebook URL
|
|
761
1062
|
let resolvedNotebookUrl = notebook_url;
|
|
@@ -819,6 +1120,9 @@ export class ToolHandlers {
|
|
|
819
1120
|
// Progress: Getting or creating session
|
|
820
1121
|
await sendProgress?.('Getting or creating browser session...', 1, 5);
|
|
821
1122
|
// Apply browser options temporarily
|
|
1123
|
+
// NOTE: This pattern is not fully thread-safe for concurrent requests.
|
|
1124
|
+
// The overrideHeadless parameter passed to getOrCreateSession handles the critical
|
|
1125
|
+
// browser visibility setting. Future improvement: pass config through function chain.
|
|
822
1126
|
const originalConfig = { ...CONFIG };
|
|
823
1127
|
const effectiveConfig = applyBrowserOptions(browser_options, show_browser);
|
|
824
1128
|
Object.assign(CONFIG, effectiveConfig);
|
|
@@ -839,12 +1143,22 @@ export class ToolHandlers {
|
|
|
839
1143
|
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl, overrideHeadless);
|
|
840
1144
|
// Progress: Asking question
|
|
841
1145
|
await sendProgress?.('Asking question to NotebookLM...', 2, 5);
|
|
842
|
-
// Ask the question
|
|
843
|
-
const
|
|
1146
|
+
// Ask the question with optional source extraction
|
|
1147
|
+
const askResult = await session.ask(question, sendProgress, source_format);
|
|
844
1148
|
// Note: FOLLOW_UP_REMINDER removed for cleaner responses
|
|
845
|
-
const answer =
|
|
1149
|
+
const answer = askResult.answer.trimEnd();
|
|
846
1150
|
// Get session info
|
|
847
1151
|
const sessionInfo = session.getInfo();
|
|
1152
|
+
// Build source citations if extracted
|
|
1153
|
+
let sources;
|
|
1154
|
+
if (askResult.citationResult && source_format !== 'none') {
|
|
1155
|
+
sources = {
|
|
1156
|
+
format: source_format,
|
|
1157
|
+
citations: askResult.citationResult.citations,
|
|
1158
|
+
extraction_success: askResult.citationResult.success,
|
|
1159
|
+
extraction_error: askResult.citationResult.error,
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
848
1162
|
const result = {
|
|
849
1163
|
status: 'success',
|
|
850
1164
|
question,
|
|
@@ -856,6 +1170,7 @@ export class ToolHandlers {
|
|
|
856
1170
|
message_count: sessionInfo.message_count,
|
|
857
1171
|
last_activity: sessionInfo.last_activity,
|
|
858
1172
|
},
|
|
1173
|
+
sources,
|
|
859
1174
|
};
|
|
860
1175
|
// Progress: Complete
|
|
861
1176
|
await sendProgress?.('Question answered successfully!', 5, 5);
|
|
@@ -872,12 +1187,99 @@ export class ToolHandlers {
|
|
|
872
1187
|
}
|
|
873
1188
|
catch (error) {
|
|
874
1189
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
875
|
-
// Special handling for rate limit errors
|
|
876
|
-
if (error instanceof RateLimitError ||
|
|
877
|
-
|
|
1190
|
+
// Special handling for rate limit errors - try automatic account rotation
|
|
1191
|
+
if (error instanceof RateLimitError ||
|
|
1192
|
+
errorMessage.toLowerCase().includes('rate limit') ||
|
|
1193
|
+
errorMessage.toLowerCase().includes('limite quotidienne')) {
|
|
1194
|
+
log.warning(`🚫 [TOOL] Rate limit detected - attempting account rotation...`);
|
|
1195
|
+
try {
|
|
1196
|
+
const accountManager = await getAccountManager();
|
|
1197
|
+
// First, identify and mark the current rate-limited account
|
|
1198
|
+
const currentAccountId = await accountManager.getCurrentAccountId();
|
|
1199
|
+
if (currentAccountId) {
|
|
1200
|
+
log.info(` 🚫 Marking current account as rate-limited: ${currentAccountId}`);
|
|
1201
|
+
await accountManager.markRateLimited(currentAccountId);
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
log.warning(` ⚠️ No current account ID stored - marking best available as rate-limited`);
|
|
1205
|
+
const currentAccount = await accountManager.getBestAccount();
|
|
1206
|
+
if (currentAccount?.account) {
|
|
1207
|
+
await accountManager.markRateLimited(currentAccount.account.config.id);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Now get the next available account (current one is now excluded due to quota)
|
|
1211
|
+
const nextAccount = await accountManager.getBestAccount(currentAccountId || undefined);
|
|
1212
|
+
if (nextAccount && nextAccount.account) {
|
|
1213
|
+
const accountId = nextAccount.account.config.id;
|
|
1214
|
+
const email = nextAccount.account.config.email;
|
|
1215
|
+
log.info(` 🔄 Switching to account: ${email} (${accountId})`);
|
|
1216
|
+
// FIRST: Close all existing sessions AND shared context to release Chrome profile lock
|
|
1217
|
+
log.info(` 🛑 Closing sessions and browser context to release profile lock...`);
|
|
1218
|
+
await this.sessionManager.closeAllSessions();
|
|
1219
|
+
// Wait for Chrome to fully release the profile
|
|
1220
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1221
|
+
// NOW perform auto-login with the new account (profile is unlocked)
|
|
1222
|
+
const autoLogin = new AutoLoginManager(accountManager);
|
|
1223
|
+
const loginResult = await autoLogin.performAutoLogin(accountId, { showBrowser: false });
|
|
1224
|
+
if (loginResult.success) {
|
|
1225
|
+
log.success(` ✅ Switched to new account successfully`);
|
|
1226
|
+
// CRITICAL: Sync the new account's profile to the main profile
|
|
1227
|
+
const account = accountManager.getAccount(accountId);
|
|
1228
|
+
if (account) {
|
|
1229
|
+
const mainStateFile = path.join(CONFIG.dataDir, 'browser_state', 'state.json');
|
|
1230
|
+
const mainProfileDir = path.join(CONFIG.dataDir, 'chrome_profile');
|
|
1231
|
+
const accountStateFile = account.stateFilePath;
|
|
1232
|
+
const accountProfileDir = account.profileDir;
|
|
1233
|
+
log.info(` 📋 Syncing account profile to main profile...`);
|
|
1234
|
+
// Sync state.json
|
|
1235
|
+
try {
|
|
1236
|
+
await fs.promises.copyFile(accountStateFile, mainStateFile);
|
|
1237
|
+
log.success(` ✅ State synced: ${accountStateFile} → ${mainStateFile}`);
|
|
1238
|
+
}
|
|
1239
|
+
catch (e) {
|
|
1240
|
+
log.warning(` ⚠️ Could not sync state: ${e}`);
|
|
1241
|
+
}
|
|
1242
|
+
// Sync Chrome profile (delete old, copy new)
|
|
1243
|
+
try {
|
|
1244
|
+
await fs.promises.rm(mainProfileDir, { recursive: true, force: true });
|
|
1245
|
+
await fs.promises.cp(accountProfileDir, mainProfileDir, { recursive: true });
|
|
1246
|
+
log.success(` ✅ Chrome profile synced: ${accountProfileDir} → ${mainProfileDir}`);
|
|
1247
|
+
}
|
|
1248
|
+
catch (e) {
|
|
1249
|
+
log.warning(` ⚠️ Could not sync profile: ${e}`);
|
|
1250
|
+
}
|
|
1251
|
+
// Save the current account ID for future reference
|
|
1252
|
+
await accountManager.saveCurrentAccountId(accountId);
|
|
1253
|
+
}
|
|
1254
|
+
log.info(` 🔄 Retrying question with new account...`);
|
|
1255
|
+
// Retry the question with new account (only once to avoid infinite loops)
|
|
1256
|
+
const retryArgs = {
|
|
1257
|
+
...args,
|
|
1258
|
+
_retryCount: (args._retryCount || 0) + 1,
|
|
1259
|
+
};
|
|
1260
|
+
if (retryArgs._retryCount <= 3) {
|
|
1261
|
+
return this.handleAskQuestion(retryArgs, sendProgress);
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
log.warning(` ⚠️ Max retry count reached (${retryArgs._retryCount})`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
log.error(` ❌ Failed to switch account: ${loginResult.error}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
log.warning(` ⚠️ No other accounts available for rotation`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
catch (rotationError) {
|
|
1276
|
+
log.error(` ❌ Account rotation failed: ${rotationError}`);
|
|
1277
|
+
}
|
|
1278
|
+
// If rotation failed, return the original error
|
|
878
1279
|
return {
|
|
879
1280
|
success: false,
|
|
880
1281
|
error: 'NotebookLM rate limit reached (50 queries/day for free accounts).\n\n' +
|
|
1282
|
+
'Automatic account rotation failed or no other accounts available.\n\n' +
|
|
881
1283
|
'You can:\n' +
|
|
882
1284
|
"1. Use the 're_auth' tool to login with a different Google account\n" +
|
|
883
1285
|
'2. Wait until tomorrow for the quota to reset\n' +
|
|
@@ -1547,6 +1949,625 @@ export class ToolHandlers {
|
|
|
1547
1949
|
};
|
|
1548
1950
|
}
|
|
1549
1951
|
}
|
|
1952
|
+
// ============================================================================
|
|
1953
|
+
// Content Management Handlers
|
|
1954
|
+
// ============================================================================
|
|
1955
|
+
/**
|
|
1956
|
+
* Handle add_source tool
|
|
1957
|
+
*/
|
|
1958
|
+
async handleAddSource(args) {
|
|
1959
|
+
const { source_type, file_path, url, text, title, notebook_url, session_id, show_browser } = args;
|
|
1960
|
+
log.info(`🔧 [TOOL] add_source called`);
|
|
1961
|
+
log.info(` Source type: ${source_type}`);
|
|
1962
|
+
// Apply show_browser option
|
|
1963
|
+
// show_browser=true → overrideHeadless=false (visible browser)
|
|
1964
|
+
// show_browser=false → overrideHeadless=true (headless)
|
|
1965
|
+
// show_browser=undefined → overrideHeadless=undefined (use config default)
|
|
1966
|
+
const overrideHeadless = show_browser !== undefined ? !show_browser : undefined;
|
|
1967
|
+
try {
|
|
1968
|
+
// Resolve notebook URL
|
|
1969
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
1970
|
+
if (!resolvedNotebookUrl) {
|
|
1971
|
+
return {
|
|
1972
|
+
success: false,
|
|
1973
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
// Get or create session
|
|
1977
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl, overrideHeadless);
|
|
1978
|
+
const page = session.getPage();
|
|
1979
|
+
if (!page) {
|
|
1980
|
+
return {
|
|
1981
|
+
success: false,
|
|
1982
|
+
error: 'Could not access browser page - session may not be initialized',
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
// Create content manager
|
|
1986
|
+
const contentManager = new ContentManager(page);
|
|
1987
|
+
// Add source
|
|
1988
|
+
const result = await contentManager.addSource({
|
|
1989
|
+
type: source_type,
|
|
1990
|
+
filePath: file_path,
|
|
1991
|
+
url,
|
|
1992
|
+
text,
|
|
1993
|
+
title,
|
|
1994
|
+
});
|
|
1995
|
+
if (result.success) {
|
|
1996
|
+
log.success(`✅ [TOOL] add_source completed`);
|
|
1997
|
+
}
|
|
1998
|
+
else {
|
|
1999
|
+
log.error(`❌ [TOOL] add_source failed: ${result.error}`);
|
|
2000
|
+
}
|
|
2001
|
+
return {
|
|
2002
|
+
success: result.success,
|
|
2003
|
+
data: result,
|
|
2004
|
+
error: result.error,
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
catch (error) {
|
|
2008
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2009
|
+
log.error(`❌ [TOOL] add_source failed: ${errorMessage}`);
|
|
2010
|
+
return {
|
|
2011
|
+
success: false,
|
|
2012
|
+
error: errorMessage,
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Handle delete_source tool
|
|
2018
|
+
*/
|
|
2019
|
+
async handleDeleteSource(args) {
|
|
2020
|
+
const { source_id, source_name, notebook_url, session_id } = args;
|
|
2021
|
+
log.info(`🔧 [TOOL] delete_source called`);
|
|
2022
|
+
if (source_id) {
|
|
2023
|
+
log.info(` Source ID: ${source_id}`);
|
|
2024
|
+
}
|
|
2025
|
+
if (source_name) {
|
|
2026
|
+
log.info(` Source name: ${source_name}`);
|
|
2027
|
+
}
|
|
2028
|
+
// Validate that at least one identifier is provided
|
|
2029
|
+
if (!source_id && !source_name) {
|
|
2030
|
+
return {
|
|
2031
|
+
success: false,
|
|
2032
|
+
error: 'Either source_id or source_name is required to identify the source to delete',
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
try {
|
|
2036
|
+
// Resolve notebook URL
|
|
2037
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2038
|
+
if (!resolvedNotebookUrl) {
|
|
2039
|
+
return {
|
|
2040
|
+
success: false,
|
|
2041
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
// Get or create session
|
|
2045
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2046
|
+
const page = session.getPage();
|
|
2047
|
+
if (!page) {
|
|
2048
|
+
return {
|
|
2049
|
+
success: false,
|
|
2050
|
+
error: 'Could not access browser page - session may not be initialized',
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
// Create content manager
|
|
2054
|
+
const contentManager = new ContentManager(page);
|
|
2055
|
+
// Delete source
|
|
2056
|
+
const result = await contentManager.deleteSource({
|
|
2057
|
+
sourceId: source_id,
|
|
2058
|
+
sourceName: source_name,
|
|
2059
|
+
});
|
|
2060
|
+
if (result.success) {
|
|
2061
|
+
log.success(`✅ [TOOL] delete_source completed: ${result.sourceName || result.sourceId}`);
|
|
2062
|
+
}
|
|
2063
|
+
else {
|
|
2064
|
+
log.error(`❌ [TOOL] delete_source failed: ${result.error}`);
|
|
2065
|
+
}
|
|
2066
|
+
return {
|
|
2067
|
+
success: result.success,
|
|
2068
|
+
data: result,
|
|
2069
|
+
error: result.error,
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
catch (error) {
|
|
2073
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2074
|
+
log.error(`❌ [TOOL] delete_source failed: ${errorMessage}`);
|
|
2075
|
+
return {
|
|
2076
|
+
success: false,
|
|
2077
|
+
error: errorMessage,
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Handle generate_content tool
|
|
2083
|
+
*/
|
|
2084
|
+
async handleGenerateContent(args) {
|
|
2085
|
+
const { content_type, custom_instructions, notebook_url, session_id, language, video_style, video_format, infographic_format, report_format, presentation_style, presentation_length, } = args;
|
|
2086
|
+
log.info(`🔧 [TOOL] generate_content called`);
|
|
2087
|
+
log.info(` Content type: ${content_type}`);
|
|
2088
|
+
if (language) {
|
|
2089
|
+
log.info(` Language: ${language}`);
|
|
2090
|
+
}
|
|
2091
|
+
if (video_style) {
|
|
2092
|
+
log.info(` Video style: ${video_style}`);
|
|
2093
|
+
}
|
|
2094
|
+
if (video_format) {
|
|
2095
|
+
log.info(` Video format: ${video_format}`);
|
|
2096
|
+
}
|
|
2097
|
+
try {
|
|
2098
|
+
// Resolve notebook URL
|
|
2099
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2100
|
+
if (!resolvedNotebookUrl) {
|
|
2101
|
+
return {
|
|
2102
|
+
success: false,
|
|
2103
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
// Get or create session
|
|
2107
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2108
|
+
const page = session.getPage();
|
|
2109
|
+
if (!page) {
|
|
2110
|
+
return {
|
|
2111
|
+
success: false,
|
|
2112
|
+
error: 'Could not access browser page',
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
// Create content manager
|
|
2116
|
+
const contentManager = new ContentManager(page);
|
|
2117
|
+
// Generate content with all options
|
|
2118
|
+
const result = await contentManager.generateContent({
|
|
2119
|
+
type: content_type,
|
|
2120
|
+
customInstructions: custom_instructions,
|
|
2121
|
+
language,
|
|
2122
|
+
videoStyle: video_style,
|
|
2123
|
+
videoFormat: video_format,
|
|
2124
|
+
infographicFormat: infographic_format,
|
|
2125
|
+
reportFormat: report_format,
|
|
2126
|
+
presentationStyle: presentation_style,
|
|
2127
|
+
presentationLength: presentation_length,
|
|
2128
|
+
});
|
|
2129
|
+
if (result.success) {
|
|
2130
|
+
log.success(`✅ [TOOL] generate_content completed`);
|
|
2131
|
+
}
|
|
2132
|
+
else {
|
|
2133
|
+
log.error(`❌ [TOOL] generate_content failed: ${result.error}`);
|
|
2134
|
+
}
|
|
2135
|
+
return {
|
|
2136
|
+
success: result.success,
|
|
2137
|
+
data: result,
|
|
2138
|
+
error: result.error,
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
catch (error) {
|
|
2142
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2143
|
+
log.error(`❌ [TOOL] generate_content failed: ${errorMessage}`);
|
|
2144
|
+
return {
|
|
2145
|
+
success: false,
|
|
2146
|
+
error: errorMessage,
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Handle list_content tool
|
|
2152
|
+
*/
|
|
2153
|
+
async handleListContent(args) {
|
|
2154
|
+
const { notebook_url, session_id } = args;
|
|
2155
|
+
log.info(`🔧 [TOOL] list_content called`);
|
|
2156
|
+
try {
|
|
2157
|
+
// Resolve notebook URL
|
|
2158
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2159
|
+
if (!resolvedNotebookUrl) {
|
|
2160
|
+
return {
|
|
2161
|
+
success: false,
|
|
2162
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
// Get or create session
|
|
2166
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2167
|
+
const page = session.getPage();
|
|
2168
|
+
if (!page) {
|
|
2169
|
+
return {
|
|
2170
|
+
success: false,
|
|
2171
|
+
error: 'Could not access browser page',
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
// Create content manager
|
|
2175
|
+
const contentManager = new ContentManager(page);
|
|
2176
|
+
// Get content overview
|
|
2177
|
+
const result = await contentManager.getContentOverview();
|
|
2178
|
+
log.success(`✅ [TOOL] list_content completed (${result.sourceCount} sources)`);
|
|
2179
|
+
return {
|
|
2180
|
+
success: true,
|
|
2181
|
+
data: result,
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
catch (error) {
|
|
2185
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2186
|
+
log.error(`❌ [TOOL] list_content failed: ${errorMessage}`);
|
|
2187
|
+
return {
|
|
2188
|
+
success: false,
|
|
2189
|
+
error: errorMessage,
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Handle download_content tool (generic download for audio, video, infographic)
|
|
2195
|
+
*/
|
|
2196
|
+
async handleDownloadContent(args) {
|
|
2197
|
+
const { content_type, output_path, notebook_url, session_id } = args;
|
|
2198
|
+
log.info(`🔧 [TOOL] download_content called`);
|
|
2199
|
+
log.info(` Content type: ${content_type}`);
|
|
2200
|
+
try {
|
|
2201
|
+
// Resolve notebook URL
|
|
2202
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2203
|
+
if (!resolvedNotebookUrl) {
|
|
2204
|
+
return {
|
|
2205
|
+
success: false,
|
|
2206
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
// Get or create session
|
|
2210
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2211
|
+
const page = session.getPage();
|
|
2212
|
+
if (!page) {
|
|
2213
|
+
return {
|
|
2214
|
+
success: false,
|
|
2215
|
+
error: 'Could not access browser page',
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
// Create content manager
|
|
2219
|
+
const contentManager = new ContentManager(page);
|
|
2220
|
+
// Download/export content
|
|
2221
|
+
const result = await contentManager.downloadContent(content_type, output_path);
|
|
2222
|
+
if (result.success) {
|
|
2223
|
+
// Log appropriate message based on export type
|
|
2224
|
+
if (result.googleSlidesUrl) {
|
|
2225
|
+
log.success(`✅ [TOOL] download_content completed: Google Slides URL exported`);
|
|
2226
|
+
}
|
|
2227
|
+
else if (result.googleSheetsUrl) {
|
|
2228
|
+
log.success(`✅ [TOOL] download_content completed: Google Sheets URL exported`);
|
|
2229
|
+
}
|
|
2230
|
+
else if (result.filePath) {
|
|
2231
|
+
log.success(`✅ [TOOL] download_content completed: ${result.filePath}`);
|
|
2232
|
+
}
|
|
2233
|
+
else {
|
|
2234
|
+
log.success(`✅ [TOOL] download_content completed`);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
else {
|
|
2238
|
+
log.error(`❌ [TOOL] download_content failed: ${result.error}`);
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
success: result.success,
|
|
2242
|
+
data: result,
|
|
2243
|
+
error: result.error,
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
catch (error) {
|
|
2247
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2248
|
+
log.error(`❌ [TOOL] download_content failed: ${errorMessage}`);
|
|
2249
|
+
return {
|
|
2250
|
+
success: false,
|
|
2251
|
+
error: errorMessage,
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Handle create_note tool
|
|
2257
|
+
*
|
|
2258
|
+
* Creates a note in the NotebookLM Studio panel with the specified title and content.
|
|
2259
|
+
*/
|
|
2260
|
+
async handleCreateNote(args) {
|
|
2261
|
+
const { title, content, notebook_url, session_id } = args;
|
|
2262
|
+
log.info(`🔧 [TOOL] create_note called`);
|
|
2263
|
+
log.info(` Title: "${title}"`);
|
|
2264
|
+
log.info(` Content length: ${content.length} chars`);
|
|
2265
|
+
try {
|
|
2266
|
+
// Validate required fields
|
|
2267
|
+
if (!title || title.trim().length === 0) {
|
|
2268
|
+
return {
|
|
2269
|
+
success: false,
|
|
2270
|
+
error: 'Note title is required',
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
if (!content || content.trim().length === 0) {
|
|
2274
|
+
return {
|
|
2275
|
+
success: false,
|
|
2276
|
+
error: 'Note content is required',
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
// Resolve notebook URL
|
|
2280
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2281
|
+
if (!resolvedNotebookUrl) {
|
|
2282
|
+
return {
|
|
2283
|
+
success: false,
|
|
2284
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
// Get or create session
|
|
2288
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2289
|
+
const page = session.getPage();
|
|
2290
|
+
if (!page) {
|
|
2291
|
+
return {
|
|
2292
|
+
success: false,
|
|
2293
|
+
error: 'Could not access browser page',
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
// Create content manager
|
|
2297
|
+
const contentManager = new ContentManager(page);
|
|
2298
|
+
// Create the note
|
|
2299
|
+
const result = await contentManager.createNote({
|
|
2300
|
+
title: title.trim(),
|
|
2301
|
+
content: content.trim(),
|
|
2302
|
+
});
|
|
2303
|
+
if (result.success) {
|
|
2304
|
+
log.success(`✅ [TOOL] create_note completed: "${title}"`);
|
|
2305
|
+
}
|
|
2306
|
+
else {
|
|
2307
|
+
log.error(`❌ [TOOL] create_note failed: ${result.error}`);
|
|
2308
|
+
}
|
|
2309
|
+
return {
|
|
2310
|
+
success: result.success,
|
|
2311
|
+
data: result,
|
|
2312
|
+
error: result.error,
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
catch (error) {
|
|
2316
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2317
|
+
log.error(`❌ [TOOL] create_note failed: ${errorMessage}`);
|
|
2318
|
+
return {
|
|
2319
|
+
success: false,
|
|
2320
|
+
error: errorMessage,
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Handle save_chat_to_note tool
|
|
2326
|
+
*
|
|
2327
|
+
* Extracts chat messages from the Discussion panel and saves them as a note.
|
|
2328
|
+
*/
|
|
2329
|
+
async handleSaveChatToNote(args) {
|
|
2330
|
+
const { title, notebook_url, session_id } = args;
|
|
2331
|
+
log.info(`🔧 [TOOL] save_chat_to_note called`);
|
|
2332
|
+
if (title) {
|
|
2333
|
+
log.info(` Title: "${title}"`);
|
|
2334
|
+
}
|
|
2335
|
+
try {
|
|
2336
|
+
// Resolve notebook URL
|
|
2337
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2338
|
+
if (!resolvedNotebookUrl) {
|
|
2339
|
+
return {
|
|
2340
|
+
success: false,
|
|
2341
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
// Get or create session
|
|
2345
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2346
|
+
const page = session.getPage();
|
|
2347
|
+
if (!page) {
|
|
2348
|
+
return {
|
|
2349
|
+
success: false,
|
|
2350
|
+
error: 'Could not access browser page',
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
// Create content manager
|
|
2354
|
+
const contentManager = new ContentManager(page);
|
|
2355
|
+
// Save chat to note
|
|
2356
|
+
const result = await contentManager.saveChatToNote({
|
|
2357
|
+
title,
|
|
2358
|
+
});
|
|
2359
|
+
if (result.success) {
|
|
2360
|
+
log.success(`✅ [TOOL] save_chat_to_note completed: "${result.noteTitle}" (${result.messageCount} messages)`);
|
|
2361
|
+
}
|
|
2362
|
+
else {
|
|
2363
|
+
log.error(`❌ [TOOL] save_chat_to_note failed: ${result.error}`);
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
success: result.success,
|
|
2367
|
+
data: result,
|
|
2368
|
+
error: result.error,
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
catch (error) {
|
|
2372
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2373
|
+
log.error(`❌ [TOOL] save_chat_to_note failed: ${errorMessage}`);
|
|
2374
|
+
return {
|
|
2375
|
+
success: false,
|
|
2376
|
+
error: errorMessage,
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Handle convert_note_to_source tool
|
|
2382
|
+
*
|
|
2383
|
+
* Converts an existing note to a source document in NotebookLM.
|
|
2384
|
+
* This makes the note content available for RAG queries.
|
|
2385
|
+
*/
|
|
2386
|
+
async handleConvertNoteToSource(args) {
|
|
2387
|
+
const { note_title, notebook_url, session_id } = args;
|
|
2388
|
+
log.info(`🔧 [TOOL] convert_note_to_source called`);
|
|
2389
|
+
log.info(` Note title: "${note_title}"`);
|
|
2390
|
+
try {
|
|
2391
|
+
// Validate required fields
|
|
2392
|
+
if (!note_title || note_title.trim().length === 0) {
|
|
2393
|
+
return {
|
|
2394
|
+
success: false,
|
|
2395
|
+
error: 'Note title is required',
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
// Resolve notebook URL
|
|
2399
|
+
const resolvedNotebookUrl = notebook_url || this.library.getActiveNotebook()?.url || CONFIG.notebookUrl;
|
|
2400
|
+
if (!resolvedNotebookUrl) {
|
|
2401
|
+
return {
|
|
2402
|
+
success: false,
|
|
2403
|
+
error: 'No notebook URL provided and no active notebook set',
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
// Get or create session
|
|
2407
|
+
const session = await this.sessionManager.getOrCreateSession(session_id, resolvedNotebookUrl);
|
|
2408
|
+
const page = session.getPage();
|
|
2409
|
+
if (!page) {
|
|
2410
|
+
return {
|
|
2411
|
+
success: false,
|
|
2412
|
+
error: 'Could not access browser page',
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
// Create content manager
|
|
2416
|
+
const contentManager = new ContentManager(page);
|
|
2417
|
+
// Convert the note to source
|
|
2418
|
+
const result = await contentManager.convertNoteToSource({
|
|
2419
|
+
noteTitle: note_title.trim(),
|
|
2420
|
+
});
|
|
2421
|
+
if (result.success) {
|
|
2422
|
+
log.success(`✅ [TOOL] convert_note_to_source completed: "${note_title}" -> "${result.sourceName}"`);
|
|
2423
|
+
}
|
|
2424
|
+
else {
|
|
2425
|
+
log.error(`❌ [TOOL] convert_note_to_source failed: ${result.error}`);
|
|
2426
|
+
}
|
|
2427
|
+
return {
|
|
2428
|
+
success: result.success,
|
|
2429
|
+
data: result,
|
|
2430
|
+
error: result.error,
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
catch (error) {
|
|
2434
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2435
|
+
log.error(`❌ [TOOL] convert_note_to_source failed: ${errorMessage}`);
|
|
2436
|
+
return {
|
|
2437
|
+
success: false,
|
|
2438
|
+
error: errorMessage,
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Handle create_notebook tool
|
|
2444
|
+
*
|
|
2445
|
+
* Creates a new empty notebook in NotebookLM via browser automation.
|
|
2446
|
+
* Returns the URL of the newly created notebook.
|
|
2447
|
+
*/
|
|
2448
|
+
async handleCreateNotebook(args, sendProgress) {
|
|
2449
|
+
const { name, show_browser } = args;
|
|
2450
|
+
await sendProgress?.('Creating new notebook...', 0, 5);
|
|
2451
|
+
log.info(`🔧 [TOOL] create_notebook called`);
|
|
2452
|
+
try {
|
|
2453
|
+
// Apply show_browser option
|
|
2454
|
+
const originalHeadless = CONFIG.headless;
|
|
2455
|
+
if (show_browser !== undefined) {
|
|
2456
|
+
CONFIG.headless = !show_browser;
|
|
2457
|
+
}
|
|
2458
|
+
// Get shared context manager
|
|
2459
|
+
const sharedContextManager = this.sessionManager.getSharedContextManager();
|
|
2460
|
+
const context = await sharedContextManager.getOrCreateContext();
|
|
2461
|
+
const page = await context.newPage();
|
|
2462
|
+
try {
|
|
2463
|
+
await sendProgress?.('Navigating to NotebookLM...', 1, 5);
|
|
2464
|
+
log.info(' 📄 Navigating to NotebookLM homepage...');
|
|
2465
|
+
// Navigate to NotebookLM homepage
|
|
2466
|
+
await page.goto('https://notebooklm.google.com/', {
|
|
2467
|
+
waitUntil: 'networkidle',
|
|
2468
|
+
timeout: 30000,
|
|
2469
|
+
});
|
|
2470
|
+
await randomDelay(1500, 2500);
|
|
2471
|
+
await sendProgress?.('Clicking create button...', 2, 5);
|
|
2472
|
+
log.info(' 🖱️ Looking for Create notebook button...');
|
|
2473
|
+
// Look for "Create" or "Créer" button
|
|
2474
|
+
const createButtonSelectors = [
|
|
2475
|
+
'button:has-text("Create")',
|
|
2476
|
+
'button:has-text("Créer")',
|
|
2477
|
+
'button:has-text("New notebook")',
|
|
2478
|
+
'button:has-text("Nouveau")',
|
|
2479
|
+
'[aria-label*="Create"]',
|
|
2480
|
+
'[aria-label*="Créer"]',
|
|
2481
|
+
'.create-notebook-button',
|
|
2482
|
+
'button.mdc-button:has-text("Create")',
|
|
2483
|
+
];
|
|
2484
|
+
let clicked = false;
|
|
2485
|
+
for (const selector of createButtonSelectors) {
|
|
2486
|
+
try {
|
|
2487
|
+
const btn = page.locator(selector).first();
|
|
2488
|
+
if (await btn.isVisible({ timeout: 2000 })) {
|
|
2489
|
+
await btn.click();
|
|
2490
|
+
clicked = true;
|
|
2491
|
+
log.success(` ✅ Clicked: ${selector}`);
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
catch {
|
|
2496
|
+
// Try next selector
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (!clicked) {
|
|
2500
|
+
// Try finding any button with "+" icon or create text
|
|
2501
|
+
const allButtons = await page.locator('button').all();
|
|
2502
|
+
for (const btn of allButtons) {
|
|
2503
|
+
const text = await btn.textContent();
|
|
2504
|
+
if (text && (text.includes('Create') || text.includes('Créer') || text.includes('+'))) {
|
|
2505
|
+
await btn.click();
|
|
2506
|
+
clicked = true;
|
|
2507
|
+
log.success(` ✅ Clicked button with text: ${text}`);
|
|
2508
|
+
break;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
if (!clicked) {
|
|
2513
|
+
throw new Error('Could not find Create notebook button');
|
|
2514
|
+
}
|
|
2515
|
+
await sendProgress?.('Waiting for notebook creation...', 3, 5);
|
|
2516
|
+
log.info(' ⏳ Waiting for new notebook to be created...');
|
|
2517
|
+
// Wait for navigation to new notebook
|
|
2518
|
+
await page.waitForURL(/notebooklm\.google\.com\/notebook\//, { timeout: 30000 });
|
|
2519
|
+
await randomDelay(2000, 3000);
|
|
2520
|
+
// Get the new notebook URL
|
|
2521
|
+
const notebookUrl = page.url();
|
|
2522
|
+
const notebookIdMatch = notebookUrl.match(/notebook\/([a-f0-9-]+)/);
|
|
2523
|
+
const notebookId = notebookIdMatch ? notebookIdMatch[1] : 'unknown';
|
|
2524
|
+
await sendProgress?.('Notebook created!', 4, 5);
|
|
2525
|
+
log.success(` ✅ New notebook created: ${notebookUrl}`);
|
|
2526
|
+
// If name provided, try to rename the notebook
|
|
2527
|
+
if (name) {
|
|
2528
|
+
log.info(` 📝 Renaming notebook to: ${name}`);
|
|
2529
|
+
try {
|
|
2530
|
+
// Click on notebook title to edit
|
|
2531
|
+
const titleSelector = '[contenteditable="true"], .notebook-title, h1';
|
|
2532
|
+
const titleEl = page.locator(titleSelector).first();
|
|
2533
|
+
if (await titleEl.isVisible({ timeout: 3000 })) {
|
|
2534
|
+
await titleEl.click();
|
|
2535
|
+
await page.keyboard.press('Control+a');
|
|
2536
|
+
await page.keyboard.type(name, { delay: 50 });
|
|
2537
|
+
await page.keyboard.press('Escape');
|
|
2538
|
+
log.success(` ✅ Notebook renamed to: ${name}`);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
catch (e) {
|
|
2542
|
+
log.warning(` ⚠️ Could not rename notebook: ${e}`);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
await sendProgress?.('Done!', 5, 5);
|
|
2546
|
+
// Restore headless config
|
|
2547
|
+
CONFIG.headless = originalHeadless;
|
|
2548
|
+
return {
|
|
2549
|
+
success: true,
|
|
2550
|
+
data: {
|
|
2551
|
+
notebook_url: notebookUrl,
|
|
2552
|
+
notebook_id: notebookId,
|
|
2553
|
+
message: `Successfully created new notebook${name ? ` "${name}"` : ''}`,
|
|
2554
|
+
},
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
finally {
|
|
2558
|
+
// Close the page we created (but keep the context)
|
|
2559
|
+
await page.close();
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
catch (error) {
|
|
2563
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2564
|
+
log.error(`❌ [TOOL] create_notebook failed: ${errorMessage}`);
|
|
2565
|
+
return {
|
|
2566
|
+
success: false,
|
|
2567
|
+
error: errorMessage,
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
1550
2571
|
/**
|
|
1551
2572
|
* Cleanup all resources (called on server shutdown)
|
|
1552
2573
|
*/
|