@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.
Files changed (138) hide show
  1. package/README.md +93 -658
  2. package/dist/accounts/account-manager.d.ts +163 -0
  3. package/dist/accounts/account-manager.d.ts.map +1 -0
  4. package/dist/accounts/account-manager.js +614 -0
  5. package/dist/accounts/account-manager.js.map +1 -0
  6. package/dist/accounts/auto-login-manager.d.ts +62 -0
  7. package/dist/accounts/auto-login-manager.d.ts.map +1 -0
  8. package/dist/accounts/auto-login-manager.js +537 -0
  9. package/dist/accounts/auto-login-manager.js.map +1 -0
  10. package/dist/accounts/crypto.d.ts +45 -0
  11. package/dist/accounts/crypto.d.ts.map +1 -0
  12. package/dist/accounts/crypto.js +138 -0
  13. package/dist/accounts/crypto.js.map +1 -0
  14. package/dist/accounts/index.d.ts +14 -0
  15. package/dist/accounts/index.d.ts.map +1 -0
  16. package/dist/accounts/index.js +14 -0
  17. package/dist/accounts/index.js.map +1 -0
  18. package/dist/accounts/types.d.ts +103 -0
  19. package/dist/accounts/types.d.ts.map +1 -0
  20. package/dist/accounts/types.js +7 -0
  21. package/dist/accounts/types.js.map +1 -0
  22. package/dist/auth/auth-manager.d.ts +9 -2
  23. package/dist/auth/auth-manager.d.ts.map +1 -1
  24. package/dist/auth/auth-manager.js +60 -6
  25. package/dist/auth/auth-manager.js.map +1 -1
  26. package/dist/auto-discovery/auto-discovery.d.ts.map +1 -1
  27. package/dist/auto-discovery/auto-discovery.js +2 -1
  28. package/dist/auto-discovery/auto-discovery.js.map +1 -1
  29. package/dist/cli/accounts.d.ts +13 -0
  30. package/dist/cli/accounts.d.ts.map +1 -0
  31. package/dist/cli/accounts.js +195 -0
  32. package/dist/cli/accounts.js.map +1 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +9 -0
  35. package/dist/config.js.map +1 -1
  36. package/dist/content/content-generator.d.ts +153 -0
  37. package/dist/content/content-generator.d.ts.map +1 -0
  38. package/dist/content/content-generator.js +637 -0
  39. package/dist/content/content-generator.js.map +1 -0
  40. package/dist/content/content-manager.d.ts +364 -0
  41. package/dist/content/content-manager.d.ts.map +1 -0
  42. package/dist/content/content-manager.js +3846 -0
  43. package/dist/content/content-manager.js.map +1 -0
  44. package/dist/content/content-templates.d.ts +183 -0
  45. package/dist/content/content-templates.d.ts.map +1 -0
  46. package/dist/content/content-templates.js +719 -0
  47. package/dist/content/content-templates.js.map +1 -0
  48. package/dist/content/index.d.ts +14 -0
  49. package/dist/content/index.d.ts.map +1 -0
  50. package/dist/content/index.js +14 -0
  51. package/dist/content/index.js.map +1 -0
  52. package/dist/content/types.d.ts +285 -0
  53. package/dist/content/types.d.ts.map +1 -0
  54. package/dist/content/types.js +10 -0
  55. package/dist/content/types.js.map +1 -0
  56. package/dist/errors.d.ts +1 -1
  57. package/dist/errors.d.ts.map +1 -1
  58. package/dist/errors.js.map +1 -1
  59. package/dist/http-wrapper.d.ts +7 -0
  60. package/dist/http-wrapper.d.ts.map +1 -1
  61. package/dist/http-wrapper.js +449 -29
  62. package/dist/http-wrapper.js.map +1 -1
  63. package/dist/index.js +26 -2
  64. package/dist/index.js.map +1 -1
  65. package/dist/library/notebook-library.d.ts +4 -0
  66. package/dist/library/notebook-library.d.ts.map +1 -1
  67. package/dist/library/notebook-library.js +20 -3
  68. package/dist/library/notebook-library.js.map +1 -1
  69. package/dist/session/browser-session.d.ts +35 -8
  70. package/dist/session/browser-session.d.ts.map +1 -1
  71. package/dist/session/browser-session.js +242 -28
  72. package/dist/session/browser-session.js.map +1 -1
  73. package/dist/session/session-manager.d.ts +6 -0
  74. package/dist/session/session-manager.d.ts.map +1 -1
  75. package/dist/session/session-manager.js +46 -14
  76. package/dist/session/session-manager.js.map +1 -1
  77. package/dist/session/shared-context-manager.d.ts +3 -3
  78. package/dist/session/shared-context-manager.d.ts.map +1 -1
  79. package/dist/session/shared-context-manager.js +8 -7
  80. package/dist/session/shared-context-manager.js.map +1 -1
  81. package/dist/stdio-http-proxy.d.ts +24 -0
  82. package/dist/stdio-http-proxy.d.ts.map +1 -0
  83. package/dist/stdio-http-proxy.js +592 -0
  84. package/dist/stdio-http-proxy.js.map +1 -0
  85. package/dist/tools/index.d.ts +106 -1
  86. package/dist/tools/index.d.ts.map +1 -1
  87. package/dist/tools/index.js +1028 -7
  88. package/dist/tools/index.js.map +1 -1
  89. package/dist/types.d.ts +81 -17
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/utils/citation-extractor.d.ts +66 -0
  92. package/dist/utils/citation-extractor.d.ts.map +1 -0
  93. package/dist/utils/citation-extractor.js +492 -0
  94. package/dist/utils/citation-extractor.js.map +1 -0
  95. package/dist/utils/page-utils.d.ts +8 -0
  96. package/dist/utils/page-utils.d.ts.map +1 -1
  97. package/dist/utils/page-utils.js +112 -8
  98. package/dist/utils/page-utils.js.map +1 -1
  99. package/docs/ARCHITECTURE_MIGRATION_STUDY.md +894 -0
  100. package/docs/CHROME_PROFILE_LIMITATION.md +15 -1
  101. package/docs/MULTI_ACCOUNT_SYSTEM.md +304 -0
  102. package/package.json +10 -10
  103. package/dist/__tests__/cleanup-manager.test.d.ts +0 -2
  104. package/dist/__tests__/cleanup-manager.test.d.ts.map +0 -1
  105. package/dist/__tests__/cleanup-manager.test.js +0 -341
  106. package/dist/__tests__/cleanup-manager.test.js.map +0 -1
  107. package/dist/__tests__/config-parsing.test.d.ts +0 -2
  108. package/dist/__tests__/config-parsing.test.d.ts.map +0 -1
  109. package/dist/__tests__/config-parsing.test.js +0 -338
  110. package/dist/__tests__/config-parsing.test.js.map +0 -1
  111. package/dist/__tests__/config.test.d.ts +0 -2
  112. package/dist/__tests__/config.test.d.ts.map +0 -1
  113. package/dist/__tests__/config.test.js +0 -267
  114. package/dist/__tests__/config.test.js.map +0 -1
  115. package/dist/__tests__/errors.test.d.ts +0 -2
  116. package/dist/__tests__/errors.test.d.ts.map +0 -1
  117. package/dist/__tests__/errors.test.js +0 -166
  118. package/dist/__tests__/errors.test.js.map +0 -1
  119. package/dist/__tests__/logger.test.d.ts +0 -2
  120. package/dist/__tests__/logger.test.d.ts.map +0 -1
  121. package/dist/__tests__/logger.test.js +0 -324
  122. package/dist/__tests__/logger.test.js.map +0 -1
  123. package/dist/__tests__/page-utils.test.d.ts +0 -2
  124. package/dist/__tests__/page-utils.test.d.ts.map +0 -1
  125. package/dist/__tests__/page-utils.test.js +0 -349
  126. package/dist/__tests__/page-utils.test.js.map +0 -1
  127. package/dist/__tests__/setup-verification.test.d.ts +0 -2
  128. package/dist/__tests__/setup-verification.test.d.ts.map +0 -1
  129. package/dist/__tests__/setup-verification.test.js +0 -15
  130. package/dist/__tests__/setup-verification.test.js.map +0 -1
  131. package/dist/__tests__/stealth-utils.test.d.ts +0 -2
  132. package/dist/__tests__/stealth-utils.test.d.ts.map +0 -1
  133. package/dist/__tests__/stealth-utils.test.js +0 -413
  134. package/dist/__tests__/stealth-utils.test.js.map +0 -1
  135. package/dist/__tests__/types.test.d.ts +0 -2
  136. package/dist/__tests__/types.test.d.ts.map +0 -1
  137. package/dist/__tests__/types.test.js +0 -461
  138. package/dist/__tests__/types.test.js.map +0 -1
@@ -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 (pass progress callback)
843
- const rawAnswer = await session.ask(question, sendProgress);
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 = rawAnswer.trimEnd();
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 || errorMessage.toLowerCase().includes('rate limit')) {
877
- log.error(`🚫 [TOOL] Rate limit detected`);
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
  */