@pan-sec/notebooklm-mcp 1.6.0 → 1.8.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 (134) hide show
  1. package/dist/config.d.ts +4 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +10 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/events/event-emitter.d.ts +45 -0
  6. package/dist/events/event-emitter.d.ts.map +1 -0
  7. package/dist/events/event-emitter.js +100 -0
  8. package/dist/events/event-emitter.js.map +1 -0
  9. package/dist/events/event-types.d.ts +124 -0
  10. package/dist/events/event-types.d.ts.map +1 -0
  11. package/dist/events/event-types.js +18 -0
  12. package/dist/events/event-types.js.map +1 -0
  13. package/dist/gemini/gemini-client.d.ts +45 -0
  14. package/dist/gemini/gemini-client.d.ts.map +1 -0
  15. package/dist/gemini/gemini-client.js +211 -0
  16. package/dist/gemini/gemini-client.js.map +1 -0
  17. package/dist/gemini/index.d.ts +8 -0
  18. package/dist/gemini/index.d.ts.map +1 -0
  19. package/dist/gemini/index.js +8 -0
  20. package/dist/gemini/index.js.map +1 -0
  21. package/dist/gemini/types.d.ts +136 -0
  22. package/dist/gemini/types.d.ts.map +1 -0
  23. package/dist/gemini/types.js +10 -0
  24. package/dist/gemini/types.js.map +1 -0
  25. package/dist/index.js +76 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/library/notebook-library.d.ts +25 -2
  28. package/dist/library/notebook-library.d.ts.map +1 -1
  29. package/dist/library/notebook-library.js +142 -2
  30. package/dist/library/notebook-library.js.map +1 -1
  31. package/dist/library/types.d.ts +15 -0
  32. package/dist/library/types.d.ts.map +1 -1
  33. package/dist/notebook-creation/audio-manager.d.ts +56 -0
  34. package/dist/notebook-creation/audio-manager.d.ts.map +1 -0
  35. package/dist/notebook-creation/audio-manager.js +335 -0
  36. package/dist/notebook-creation/audio-manager.js.map +1 -0
  37. package/dist/notebook-creation/discover-creation-flow.d.ts +8 -0
  38. package/dist/notebook-creation/discover-creation-flow.d.ts.map +1 -0
  39. package/dist/notebook-creation/discover-creation-flow.js +177 -0
  40. package/dist/notebook-creation/discover-creation-flow.js.map +1 -0
  41. package/dist/notebook-creation/discover-quota.d.ts +8 -0
  42. package/dist/notebook-creation/discover-quota.d.ts.map +1 -0
  43. package/dist/notebook-creation/discover-quota.js +195 -0
  44. package/dist/notebook-creation/discover-quota.js.map +1 -0
  45. package/dist/notebook-creation/discover-source-dialog.d.ts +8 -0
  46. package/dist/notebook-creation/discover-source-dialog.d.ts.map +1 -0
  47. package/dist/notebook-creation/discover-source-dialog.js +134 -0
  48. package/dist/notebook-creation/discover-source-dialog.js.map +1 -0
  49. package/dist/notebook-creation/discover-sources.d.ts +8 -0
  50. package/dist/notebook-creation/discover-sources.d.ts.map +1 -0
  51. package/dist/notebook-creation/discover-sources.js +273 -0
  52. package/dist/notebook-creation/discover-sources.js.map +1 -0
  53. package/dist/notebook-creation/discover-text-input.d.ts +7 -0
  54. package/dist/notebook-creation/discover-text-input.d.ts.map +1 -0
  55. package/dist/notebook-creation/discover-text-input.js +135 -0
  56. package/dist/notebook-creation/discover-text-input.js.map +1 -0
  57. package/dist/notebook-creation/index.d.ts +12 -0
  58. package/dist/notebook-creation/index.d.ts.map +1 -0
  59. package/dist/notebook-creation/index.js +12 -0
  60. package/dist/notebook-creation/index.js.map +1 -0
  61. package/dist/notebook-creation/notebook-creator.d.ts +95 -0
  62. package/dist/notebook-creation/notebook-creator.d.ts.map +1 -0
  63. package/dist/notebook-creation/notebook-creator.js +689 -0
  64. package/dist/notebook-creation/notebook-creator.js.map +1 -0
  65. package/dist/notebook-creation/notebook-sync.d.ts +93 -0
  66. package/dist/notebook-creation/notebook-sync.d.ts.map +1 -0
  67. package/dist/notebook-creation/notebook-sync.js +370 -0
  68. package/dist/notebook-creation/notebook-sync.js.map +1 -0
  69. package/dist/notebook-creation/run-discovery.d.ts +11 -0
  70. package/dist/notebook-creation/run-discovery.d.ts.map +1 -0
  71. package/dist/notebook-creation/run-discovery.js +151 -0
  72. package/dist/notebook-creation/run-discovery.js.map +1 -0
  73. package/dist/notebook-creation/selector-discovery.d.ts +65 -0
  74. package/dist/notebook-creation/selector-discovery.d.ts.map +1 -0
  75. package/dist/notebook-creation/selector-discovery.js +421 -0
  76. package/dist/notebook-creation/selector-discovery.js.map +1 -0
  77. package/dist/notebook-creation/selectors.d.ts +150 -0
  78. package/dist/notebook-creation/selectors.d.ts.map +1 -0
  79. package/dist/notebook-creation/selectors.js +225 -0
  80. package/dist/notebook-creation/selectors.js.map +1 -0
  81. package/dist/notebook-creation/source-manager.d.ts +73 -0
  82. package/dist/notebook-creation/source-manager.d.ts.map +1 -0
  83. package/dist/notebook-creation/source-manager.js +486 -0
  84. package/dist/notebook-creation/source-manager.js.map +1 -0
  85. package/dist/notebook-creation/test-create.d.ts +8 -0
  86. package/dist/notebook-creation/test-create.d.ts.map +1 -0
  87. package/dist/notebook-creation/test-create.js +72 -0
  88. package/dist/notebook-creation/test-create.js.map +1 -0
  89. package/dist/notebook-creation/types.d.ts +173 -0
  90. package/dist/notebook-creation/types.d.ts.map +1 -0
  91. package/dist/notebook-creation/types.js +5 -0
  92. package/dist/notebook-creation/types.js.map +1 -0
  93. package/dist/quota/index.d.ts +8 -0
  94. package/dist/quota/index.d.ts.map +1 -0
  95. package/dist/quota/index.js +8 -0
  96. package/dist/quota/index.js.map +1 -0
  97. package/dist/quota/quota-manager.d.ts +125 -0
  98. package/dist/quota/quota-manager.d.ts.map +1 -0
  99. package/dist/quota/quota-manager.js +330 -0
  100. package/dist/quota/quota-manager.js.map +1 -0
  101. package/dist/session/session-manager.d.ts +5 -0
  102. package/dist/session/session-manager.d.ts.map +1 -1
  103. package/dist/session/session-manager.js +6 -0
  104. package/dist/session/session-manager.js.map +1 -1
  105. package/dist/tools/definitions/gemini.d.ts +12 -0
  106. package/dist/tools/definitions/gemini.d.ts.map +1 -0
  107. package/dist/tools/definitions/gemini.js +135 -0
  108. package/dist/tools/definitions/gemini.js.map +1 -0
  109. package/dist/tools/definitions/notebook-management.d.ts.map +1 -1
  110. package/dist/tools/definitions/notebook-management.js +525 -0
  111. package/dist/tools/definitions/notebook-management.js.map +1 -1
  112. package/dist/tools/definitions/system.d.ts.map +1 -1
  113. package/dist/tools/definitions/system.js +158 -0
  114. package/dist/tools/definitions/system.js.map +1 -1
  115. package/dist/tools/definitions.d.ts.map +1 -1
  116. package/dist/tools/definitions.js +2 -0
  117. package/dist/tools/definitions.js.map +1 -1
  118. package/dist/tools/handlers.d.ts +257 -0
  119. package/dist/tools/handlers.d.ts.map +1 -1
  120. package/dist/tools/handlers.js +1097 -0
  121. package/dist/tools/handlers.js.map +1 -1
  122. package/dist/webhooks/index.d.ts +8 -0
  123. package/dist/webhooks/index.d.ts.map +1 -0
  124. package/dist/webhooks/index.js +8 -0
  125. package/dist/webhooks/index.js.map +1 -0
  126. package/dist/webhooks/types.d.ts +57 -0
  127. package/dist/webhooks/types.d.ts.map +1 -0
  128. package/dist/webhooks/types.js +5 -0
  129. package/dist/webhooks/types.js.map +1 -0
  130. package/dist/webhooks/webhook-dispatcher.d.ts +120 -0
  131. package/dist/webhooks/webhook-dispatcher.d.ts.map +1 -0
  132. package/dist/webhooks/webhook-dispatcher.js +519 -0
  133. package/dist/webhooks/webhook-dispatcher.js.map +1 -0
  134. package/package.json +2 -1
@@ -10,6 +10,13 @@ import { validateNotebookUrl, validateNotebookId, validateSessionId, validateQue
10
10
  import { audit } from "../utils/audit-logger.js";
11
11
  import { validateResponse } from "../utils/response-validator.js";
12
12
  import { CleanupManager } from "../utils/cleanup-manager.js";
13
+ import { NotebookCreator } from "../notebook-creation/notebook-creator.js";
14
+ import { NotebookSync } from "../notebook-creation/notebook-sync.js";
15
+ import { SourceManager } from "../notebook-creation/source-manager.js";
16
+ import { AudioManager } from "../notebook-creation/audio-manager.js";
17
+ import { getWebhookDispatcher } from "../webhooks/index.js";
18
+ import { getQuotaManager } from "../quota/index.js";
19
+ import { GeminiClient, } from "../gemini/index.js";
13
20
  const FOLLOW_UP_REMINDER = "\n\nEXTREMELY IMPORTANT: Is that ALL you need to know? You can always ask another question using the same session ID! Think about it carefully: before you reply to the user, review their original request and this answer. If anything is still unclear or missing, ask me another question first.";
14
21
  /**
15
22
  * MCP Tool Handlers
@@ -19,12 +26,15 @@ export class ToolHandlers {
19
26
  authManager;
20
27
  library;
21
28
  rateLimiter;
29
+ geminiClient;
22
30
  constructor(sessionManager, authManager, library) {
23
31
  this.sessionManager = sessionManager;
24
32
  this.authManager = authManager;
25
33
  this.library = library;
26
34
  // Rate limit: 100 requests per minute per session (protective limit)
27
35
  this.rateLimiter = new RateLimiter(100, 60000);
36
+ // Initialize Gemini client (may be unavailable if no API key)
37
+ this.geminiClient = new GeminiClient();
28
38
  }
29
39
  /**
30
40
  * Handle ask_question tool
@@ -71,6 +81,17 @@ export class ToolHandlers {
71
81
  error: `Rate limit exceeded. Please wait before making more requests. Remaining: ${this.rateLimiter.getRemaining(rateLimitKey)}`,
72
82
  };
73
83
  }
84
+ // === QUOTA CHECK ===
85
+ const quotaManager = getQuotaManager();
86
+ const canQuery = quotaManager.canMakeQuery();
87
+ if (!canQuery.allowed) {
88
+ log.warning(`⚠️ Quota limit: ${canQuery.reason}`);
89
+ await audit.tool("ask_question", args, false, Date.now() - startTime, canQuery.reason || "Query quota exceeded");
90
+ return {
91
+ success: false,
92
+ error: canQuery.reason || "Daily query limit reached. Try again tomorrow or upgrade your plan.",
93
+ };
94
+ }
74
95
  }
75
96
  catch (error) {
76
97
  if (error instanceof SecurityError) {
@@ -173,6 +194,8 @@ export class ToolHandlers {
173
194
  // Progress: Complete
174
195
  await sendProgress?.("Question answered successfully!", 5, 5);
175
196
  log.success(`✅ [TOOL] ask_question completed successfully`);
197
+ // Update quota tracking
198
+ getQuotaManager().incrementQueryCount();
176
199
  // Audit: successful tool call
177
200
  await audit.tool("ask_question", {
178
201
  question_length: safeQuestion.length,
@@ -733,6 +756,427 @@ export class ToolHandlers {
733
756
  };
734
757
  }
735
758
  }
759
+ /**
760
+ * Handle export_library tool
761
+ *
762
+ * Exports notebook library to a backup file (JSON or CSV).
763
+ */
764
+ async handleExportLibrary(args) {
765
+ const format = args.format || "json";
766
+ log.info(`🔧 [TOOL] export_library called`);
767
+ log.info(` Format: ${format}`);
768
+ try {
769
+ const notebooks = this.library.listNotebooks();
770
+ const stats = this.library.getStats();
771
+ // Generate default output path if not provided
772
+ const date = new Date().toISOString().split("T")[0];
773
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
774
+ const defaultPath = `${homeDir}/notebooklm-library-backup-${date}.${format}`;
775
+ const outputPath = args.output_path || defaultPath;
776
+ let content;
777
+ if (format === "csv") {
778
+ // CSV format: name, url, topics, last_used, use_count
779
+ const headers = ["name", "url", "topics", "description", "last_used", "use_count"];
780
+ const rows = notebooks.map((nb) => [
781
+ `"${(nb.name || "").replace(/"/g, '""')}"`,
782
+ `"${nb.url}"`,
783
+ `"${(nb.topics || []).join("; ")}"`,
784
+ `"${(nb.description || "").replace(/"/g, '""')}"`,
785
+ nb.last_used || "",
786
+ String(nb.use_count || 0),
787
+ ]);
788
+ content = [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
789
+ }
790
+ else {
791
+ // JSON format: full library data
792
+ content = JSON.stringify({
793
+ exported_at: new Date().toISOString(),
794
+ version: "1.0",
795
+ stats: {
796
+ total_notebooks: stats.total_notebooks,
797
+ total_queries: stats.total_queries,
798
+ },
799
+ notebooks: notebooks,
800
+ }, null, 2);
801
+ }
802
+ // Write file with secure permissions
803
+ const fs = await import("fs");
804
+ fs.writeFileSync(outputPath, content, { mode: 0o600 });
805
+ const fileStats = fs.statSync(outputPath);
806
+ log.success(`✅ [TOOL] export_library completed: ${outputPath}`);
807
+ return {
808
+ success: true,
809
+ data: {
810
+ file_path: outputPath,
811
+ format,
812
+ notebook_count: notebooks.length,
813
+ size_bytes: fileStats.size,
814
+ },
815
+ };
816
+ }
817
+ catch (error) {
818
+ const errorMessage = error instanceof Error ? error.message : String(error);
819
+ log.error(`❌ [TOOL] export_library failed: ${errorMessage}`);
820
+ return {
821
+ success: false,
822
+ error: errorMessage,
823
+ };
824
+ }
825
+ }
826
+ /**
827
+ * Handle get_project_info tool
828
+ *
829
+ * Returns current project context and library location.
830
+ */
831
+ async handleGetProjectInfo() {
832
+ log.info(`🔧 [TOOL] get_project_info called`);
833
+ try {
834
+ // Get info from the library instance
835
+ const projectInfo = this.library.getProjectInfo();
836
+ const libraryPath = this.library.getLibraryPath();
837
+ const isProjectLibrary = this.library.isProjectLibrary();
838
+ // Also detect what project would be detected from cwd
839
+ const { NotebookLibrary: NL } = await import("../library/notebook-library.js");
840
+ const detectedProject = NL.detectCurrentProject();
841
+ log.success(`✅ [TOOL] get_project_info completed`);
842
+ return {
843
+ success: true,
844
+ data: {
845
+ project: projectInfo,
846
+ library_path: libraryPath,
847
+ is_project_library: isProjectLibrary,
848
+ detected_project: detectedProject,
849
+ },
850
+ };
851
+ }
852
+ catch (error) {
853
+ const errorMessage = error instanceof Error ? error.message : String(error);
854
+ log.error(`❌ [TOOL] get_project_info failed: ${errorMessage}`);
855
+ return {
856
+ success: false,
857
+ error: errorMessage,
858
+ };
859
+ }
860
+ }
861
+ /**
862
+ * Handle get_quota tool
863
+ *
864
+ * Returns current quota status including license tier, usage, and limits.
865
+ */
866
+ async handleGetQuota() {
867
+ log.info(`🔧 [TOOL] get_quota called`);
868
+ try {
869
+ const quotaManager = getQuotaManager();
870
+ const status = quotaManager.getStatus();
871
+ const settings = quotaManager.getSettings();
872
+ log.success(`✅ [TOOL] get_quota completed (tier: ${status.tier})`);
873
+ return {
874
+ success: true,
875
+ data: {
876
+ tier: status.tier,
877
+ notebooks: status.notebooks,
878
+ sources: status.sources,
879
+ queries: status.queries,
880
+ auto_detected: settings.autoDetected,
881
+ last_updated: settings.usage.lastUpdated,
882
+ },
883
+ };
884
+ }
885
+ catch (error) {
886
+ const errorMessage = error instanceof Error ? error.message : String(error);
887
+ log.error(`❌ [TOOL] get_quota failed: ${errorMessage}`);
888
+ return {
889
+ success: false,
890
+ error: errorMessage,
891
+ };
892
+ }
893
+ }
894
+ /**
895
+ * Handle set_quota_tier tool
896
+ *
897
+ * Manually set the license tier to override auto-detection.
898
+ */
899
+ async handleSetQuotaTier(args) {
900
+ log.info(`🔧 [TOOL] set_quota_tier called`);
901
+ log.info(` Tier: ${args.tier}`);
902
+ try {
903
+ const quotaManager = getQuotaManager();
904
+ quotaManager.setTier(args.tier);
905
+ const settings = quotaManager.getSettings();
906
+ log.success(`✅ [TOOL] set_quota_tier completed (tier: ${args.tier})`);
907
+ return {
908
+ success: true,
909
+ data: {
910
+ tier: settings.tier,
911
+ limits: {
912
+ notebooks: settings.limits.notebooks,
913
+ sourcesPerNotebook: settings.limits.sourcesPerNotebook,
914
+ queriesPerDay: settings.limits.queriesPerDay,
915
+ },
916
+ message: `License tier set to ${args.tier}. Limits updated accordingly.`,
917
+ },
918
+ };
919
+ }
920
+ catch (error) {
921
+ const errorMessage = error instanceof Error ? error.message : String(error);
922
+ log.error(`❌ [TOOL] set_quota_tier failed: ${errorMessage}`);
923
+ return {
924
+ success: false,
925
+ error: errorMessage,
926
+ };
927
+ }
928
+ }
929
+ /**
930
+ * Handle create_notebook tool
931
+ *
932
+ * Creates a new NotebookLM notebook with sources programmatically.
933
+ */
934
+ async handleCreateNotebook(args, sendProgress) {
935
+ log.info(`🔧 [TOOL] create_notebook called`);
936
+ log.info(` Name: ${args.name}`);
937
+ log.info(` Sources: ${args.sources?.length || 0}`);
938
+ try {
939
+ // Validate inputs
940
+ if (!args.name || typeof args.name !== "string") {
941
+ throw new Error("Notebook name is required");
942
+ }
943
+ if (!args.sources || !Array.isArray(args.sources) || args.sources.length === 0) {
944
+ throw new Error("At least one source is required");
945
+ }
946
+ // Validate each source
947
+ for (const source of args.sources) {
948
+ if (!source.type || !["url", "text", "file"].includes(source.type)) {
949
+ throw new Error(`Invalid source type: ${source.type}. Must be url, text, or file.`);
950
+ }
951
+ if (!source.value || typeof source.value !== "string") {
952
+ throw new Error("Source value is required");
953
+ }
954
+ if (source.type === "url") {
955
+ try {
956
+ new URL(source.value);
957
+ }
958
+ catch {
959
+ throw new Error(`Invalid URL: ${source.value}`);
960
+ }
961
+ }
962
+ }
963
+ // === QUOTA CHECK ===
964
+ const quotaManager = getQuotaManager();
965
+ const canCreate = quotaManager.canCreateNotebook();
966
+ if (!canCreate.allowed) {
967
+ log.warning(`⚠️ Quota limit: ${canCreate.reason}`);
968
+ return {
969
+ success: false,
970
+ error: canCreate.reason || "Notebook quota limit reached",
971
+ };
972
+ }
973
+ // Check source limit
974
+ const sourceLimits = quotaManager.getLimits();
975
+ if (args.sources.length > sourceLimits.sourcesPerNotebook) {
976
+ const reason = `Too many sources (${args.sources.length}). Limit is ${sourceLimits.sourcesPerNotebook} per notebook.`;
977
+ log.warning(`⚠️ Quota limit: ${reason}`);
978
+ return {
979
+ success: false,
980
+ error: reason,
981
+ };
982
+ }
983
+ // Get the shared context manager from session manager
984
+ const contextManager = this.sessionManager.getContextManager();
985
+ // Create notebook
986
+ const creator = new NotebookCreator(this.authManager, contextManager);
987
+ const result = await creator.createNotebook({
988
+ name: args.name,
989
+ sources: args.sources,
990
+ sendProgress,
991
+ browserOptions: args.browser_options || (args.show_browser ? { show: true } : undefined),
992
+ });
993
+ // Auto-add to library if requested (default: true)
994
+ if (args.auto_add_to_library !== false) {
995
+ try {
996
+ this.library.addNotebook({
997
+ url: result.url,
998
+ name: args.name,
999
+ description: args.description || `Created ${new Date().toLocaleDateString()}`,
1000
+ topics: args.topics || [],
1001
+ });
1002
+ log.success(`✅ Added notebook to library: ${args.name}`);
1003
+ }
1004
+ catch (libError) {
1005
+ log.warning(`⚠️ Failed to add to library: ${libError}`);
1006
+ // Don't fail the whole operation
1007
+ }
1008
+ }
1009
+ // Update quota tracking
1010
+ quotaManager.incrementNotebookCount();
1011
+ // Audit log
1012
+ await audit.tool("create_notebook", {
1013
+ name: args.name,
1014
+ sourceCount: args.sources.length,
1015
+ url: result.url,
1016
+ }, true, 0);
1017
+ log.success(`✅ [TOOL] create_notebook completed: ${result.url}`);
1018
+ return {
1019
+ success: true,
1020
+ data: result,
1021
+ };
1022
+ }
1023
+ catch (error) {
1024
+ const errorMessage = error instanceof Error ? error.message : String(error);
1025
+ log.error(`❌ [TOOL] create_notebook failed: ${errorMessage}`);
1026
+ await audit.tool("create_notebook", {
1027
+ name: args.name,
1028
+ error: errorMessage,
1029
+ }, false, 0, errorMessage);
1030
+ return {
1031
+ success: false,
1032
+ error: errorMessage,
1033
+ };
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Handle batch_create_notebooks tool
1038
+ *
1039
+ * Creates multiple notebooks in a single batch operation.
1040
+ */
1041
+ async handleBatchCreateNotebooks(args, sendProgress) {
1042
+ log.info(`🔧 [TOOL] batch_create_notebooks called`);
1043
+ log.info(` Notebooks: ${args.notebooks.length}`);
1044
+ log.info(` Stop on error: ${args.stop_on_error || false}`);
1045
+ try {
1046
+ // Validate input
1047
+ if (!args.notebooks || !Array.isArray(args.notebooks)) {
1048
+ throw new Error("notebooks array is required");
1049
+ }
1050
+ if (args.notebooks.length === 0) {
1051
+ throw new Error("At least one notebook is required");
1052
+ }
1053
+ if (args.notebooks.length > 10) {
1054
+ throw new Error("Maximum 10 notebooks per batch");
1055
+ }
1056
+ const results = [];
1057
+ const total = args.notebooks.length;
1058
+ let succeeded = 0;
1059
+ let failed = 0;
1060
+ for (let i = 0; i < args.notebooks.length; i++) {
1061
+ const notebook = args.notebooks[i];
1062
+ await sendProgress?.(`Creating notebook ${i + 1}/${total}: ${notebook.name}`, i, total);
1063
+ log.info(` 📓 Creating notebook ${i + 1}/${total}: ${notebook.name}`);
1064
+ try {
1065
+ const result = await this.handleCreateNotebook({
1066
+ name: notebook.name,
1067
+ sources: notebook.sources,
1068
+ description: notebook.description,
1069
+ topics: notebook.topics,
1070
+ auto_add_to_library: true,
1071
+ show_browser: args.show_browser,
1072
+ });
1073
+ if (result.success && result.data) {
1074
+ results.push({
1075
+ name: notebook.name,
1076
+ success: true,
1077
+ url: result.data.url,
1078
+ });
1079
+ succeeded++;
1080
+ log.success(` ✅ Created: ${result.data.url}`);
1081
+ }
1082
+ else {
1083
+ results.push({
1084
+ name: notebook.name,
1085
+ success: false,
1086
+ error: result.error || "Unknown error",
1087
+ });
1088
+ failed++;
1089
+ log.error(` ❌ Failed: ${result.error}`);
1090
+ if (args.stop_on_error) {
1091
+ log.warning(` ⚠️ Stopping batch due to error (stop_on_error=true)`);
1092
+ break;
1093
+ }
1094
+ }
1095
+ }
1096
+ catch (error) {
1097
+ const errorMessage = error instanceof Error ? error.message : String(error);
1098
+ results.push({
1099
+ name: notebook.name,
1100
+ success: false,
1101
+ error: errorMessage,
1102
+ });
1103
+ failed++;
1104
+ log.error(` ❌ Exception: ${errorMessage}`);
1105
+ if (args.stop_on_error) {
1106
+ log.warning(` ⚠️ Stopping batch due to exception (stop_on_error=true)`);
1107
+ break;
1108
+ }
1109
+ }
1110
+ // Delay between notebooks to avoid rate limiting
1111
+ if (i < args.notebooks.length - 1) {
1112
+ const delay = 2000 + Math.random() * 2000; // 2-4 seconds
1113
+ await new Promise((resolve) => setTimeout(resolve, delay));
1114
+ }
1115
+ }
1116
+ await sendProgress?.(`Batch complete: ${succeeded}/${total} succeeded`, total, total);
1117
+ log.success(`✅ [TOOL] batch_create_notebooks completed: ${succeeded}/${total} succeeded`);
1118
+ return {
1119
+ success: failed === 0,
1120
+ data: {
1121
+ total,
1122
+ succeeded,
1123
+ failed,
1124
+ results,
1125
+ },
1126
+ };
1127
+ }
1128
+ catch (error) {
1129
+ const errorMessage = error instanceof Error ? error.message : String(error);
1130
+ log.error(`❌ [TOOL] batch_create_notebooks failed: ${errorMessage}`);
1131
+ return {
1132
+ success: false,
1133
+ error: errorMessage,
1134
+ };
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Handle sync_library tool
1139
+ *
1140
+ * Syncs local library with actual NotebookLM notebooks.
1141
+ */
1142
+ async handleSyncLibrary(args) {
1143
+ log.info(`🔧 [TOOL] sync_library called`);
1144
+ log.info(` Auto-fix: ${args.auto_fix || false}`);
1145
+ log.info(` Show browser: ${args.show_browser || false}`);
1146
+ try {
1147
+ // Get the shared context manager from session manager
1148
+ const contextManager = this.sessionManager.getContextManager();
1149
+ // Sync library
1150
+ const sync = new NotebookSync(this.authManager, contextManager, this.library);
1151
+ const result = await sync.syncLibrary({
1152
+ autoFix: args.auto_fix,
1153
+ showBrowser: args.show_browser,
1154
+ });
1155
+ // Audit log
1156
+ await audit.tool("sync_library", {
1157
+ matched: result.matched.length,
1158
+ stale: result.staleEntries.length,
1159
+ missing: result.missingNotebooks.length,
1160
+ autoFix: args.auto_fix,
1161
+ }, true, 0);
1162
+ log.success(`✅ [TOOL] sync_library completed`);
1163
+ return {
1164
+ success: true,
1165
+ data: result,
1166
+ };
1167
+ }
1168
+ catch (error) {
1169
+ const errorMessage = error instanceof Error ? error.message : String(error);
1170
+ log.error(`❌ [TOOL] sync_library failed: ${errorMessage}`);
1171
+ await audit.tool("sync_library", {
1172
+ error: errorMessage,
1173
+ }, false, 0, errorMessage);
1174
+ return {
1175
+ success: false,
1176
+ error: errorMessage,
1177
+ };
1178
+ }
1179
+ }
736
1180
  /**
737
1181
  * Handle cleanup_data tool
738
1182
  *
@@ -801,6 +1245,659 @@ export class ToolHandlers {
801
1245
  };
802
1246
  }
803
1247
  }
1248
+ /**
1249
+ * Handle list_sources tool
1250
+ *
1251
+ * List all sources in a NotebookLM notebook.
1252
+ */
1253
+ async handleListSources(args) {
1254
+ log.info(`🔧 [TOOL] list_sources called`);
1255
+ try {
1256
+ // Resolve notebook URL
1257
+ let notebookUrl = args.notebook_url;
1258
+ if (!notebookUrl && args.notebook_id) {
1259
+ const notebook = this.library.getNotebook(args.notebook_id);
1260
+ if (!notebook) {
1261
+ throw new Error(`Notebook not found in library: ${args.notebook_id}`);
1262
+ }
1263
+ notebookUrl = notebook.url;
1264
+ log.info(` Resolved notebook: ${notebook.name}`);
1265
+ }
1266
+ else if (!notebookUrl) {
1267
+ const active = this.library.getActiveNotebook();
1268
+ if (active) {
1269
+ notebookUrl = active.url;
1270
+ log.info(` Using active notebook: ${active.name}`);
1271
+ }
1272
+ else {
1273
+ throw new Error("No notebook specified. Provide notebook_id or notebook_url.");
1274
+ }
1275
+ }
1276
+ // Validate URL
1277
+ const safeUrl = validateNotebookUrl(notebookUrl);
1278
+ // Get the shared context manager from session manager
1279
+ const contextManager = this.sessionManager.getContextManager();
1280
+ // List sources
1281
+ const sourceManager = new SourceManager(this.authManager, contextManager);
1282
+ const result = await sourceManager.listSources(safeUrl);
1283
+ log.success(`✅ [TOOL] list_sources completed (${result.count} sources)`);
1284
+ return {
1285
+ success: true,
1286
+ data: result,
1287
+ };
1288
+ }
1289
+ catch (error) {
1290
+ const errorMessage = error instanceof Error ? error.message : String(error);
1291
+ log.error(`❌ [TOOL] list_sources failed: ${errorMessage}`);
1292
+ return {
1293
+ success: false,
1294
+ error: errorMessage,
1295
+ };
1296
+ }
1297
+ }
1298
+ /**
1299
+ * Handle add_source tool
1300
+ *
1301
+ * Add a source to an existing NotebookLM notebook.
1302
+ */
1303
+ async handleAddSource(args) {
1304
+ log.info(`🔧 [TOOL] add_source called`);
1305
+ log.info(` Source type: ${args.source?.type}`);
1306
+ try {
1307
+ // Validate source
1308
+ if (!args.source || !args.source.type || !args.source.value) {
1309
+ throw new Error("Source with type and value is required");
1310
+ }
1311
+ if (!["url", "text", "file"].includes(args.source.type)) {
1312
+ throw new Error(`Invalid source type: ${args.source.type}. Must be url, text, or file.`);
1313
+ }
1314
+ // Resolve notebook URL
1315
+ let notebookUrl = args.notebook_url;
1316
+ if (!notebookUrl && args.notebook_id) {
1317
+ const notebook = this.library.getNotebook(args.notebook_id);
1318
+ if (!notebook) {
1319
+ throw new Error(`Notebook not found in library: ${args.notebook_id}`);
1320
+ }
1321
+ notebookUrl = notebook.url;
1322
+ log.info(` Resolved notebook: ${notebook.name}`);
1323
+ }
1324
+ else if (!notebookUrl) {
1325
+ const active = this.library.getActiveNotebook();
1326
+ if (active) {
1327
+ notebookUrl = active.url;
1328
+ log.info(` Using active notebook: ${active.name}`);
1329
+ }
1330
+ else {
1331
+ throw new Error("No notebook specified. Provide notebook_id or notebook_url.");
1332
+ }
1333
+ }
1334
+ // Validate URL
1335
+ const safeUrl = validateNotebookUrl(notebookUrl);
1336
+ // Get the shared context manager from session manager
1337
+ const contextManager = this.sessionManager.getContextManager();
1338
+ // Add source
1339
+ const sourceManager = new SourceManager(this.authManager, contextManager);
1340
+ const result = await sourceManager.addSource(safeUrl, args.source);
1341
+ if (result.success) {
1342
+ log.success(`✅ [TOOL] add_source completed`);
1343
+ }
1344
+ else {
1345
+ log.warning(`⚠️ [TOOL] add_source failed: ${result.error}`);
1346
+ }
1347
+ return {
1348
+ success: result.success,
1349
+ data: result,
1350
+ ...(result.error && { error: result.error }),
1351
+ };
1352
+ }
1353
+ catch (error) {
1354
+ const errorMessage = error instanceof Error ? error.message : String(error);
1355
+ log.error(`❌ [TOOL] add_source failed: ${errorMessage}`);
1356
+ return {
1357
+ success: false,
1358
+ error: errorMessage,
1359
+ };
1360
+ }
1361
+ }
1362
+ /**
1363
+ * Handle remove_source tool
1364
+ *
1365
+ * Remove a source from a NotebookLM notebook.
1366
+ */
1367
+ async handleRemoveSource(args) {
1368
+ log.info(`🔧 [TOOL] remove_source called`);
1369
+ log.info(` Source ID: ${args.source_id}`);
1370
+ try {
1371
+ // Validate source_id
1372
+ if (!args.source_id) {
1373
+ throw new Error("source_id is required");
1374
+ }
1375
+ // Resolve notebook URL
1376
+ let notebookUrl = args.notebook_url;
1377
+ if (!notebookUrl && args.notebook_id) {
1378
+ const notebook = this.library.getNotebook(args.notebook_id);
1379
+ if (!notebook) {
1380
+ throw new Error(`Notebook not found in library: ${args.notebook_id}`);
1381
+ }
1382
+ notebookUrl = notebook.url;
1383
+ log.info(` Resolved notebook: ${notebook.name}`);
1384
+ }
1385
+ else if (!notebookUrl) {
1386
+ const active = this.library.getActiveNotebook();
1387
+ if (active) {
1388
+ notebookUrl = active.url;
1389
+ log.info(` Using active notebook: ${active.name}`);
1390
+ }
1391
+ else {
1392
+ throw new Error("No notebook specified. Provide notebook_id or notebook_url.");
1393
+ }
1394
+ }
1395
+ // Validate URL
1396
+ const safeUrl = validateNotebookUrl(notebookUrl);
1397
+ // Get the shared context manager from session manager
1398
+ const contextManager = this.sessionManager.getContextManager();
1399
+ // Remove source
1400
+ const sourceManager = new SourceManager(this.authManager, contextManager);
1401
+ const result = await sourceManager.removeSource(safeUrl, args.source_id);
1402
+ if (result.success) {
1403
+ log.success(`✅ [TOOL] remove_source completed`);
1404
+ }
1405
+ else {
1406
+ log.warning(`⚠️ [TOOL] remove_source failed: ${result.error}`);
1407
+ }
1408
+ return {
1409
+ success: result.success,
1410
+ data: result,
1411
+ ...(result.error && { error: result.error }),
1412
+ };
1413
+ }
1414
+ catch (error) {
1415
+ const errorMessage = error instanceof Error ? error.message : String(error);
1416
+ log.error(`❌ [TOOL] remove_source failed: ${errorMessage}`);
1417
+ return {
1418
+ success: false,
1419
+ error: errorMessage,
1420
+ };
1421
+ }
1422
+ }
1423
+ /**
1424
+ * Handle generate_audio_overview tool
1425
+ *
1426
+ * Triggers audio overview generation for a notebook.
1427
+ */
1428
+ async handleGenerateAudioOverview(args) {
1429
+ log.info(`🔧 [TOOL] generate_audio_overview called`);
1430
+ try {
1431
+ // Resolve notebook URL
1432
+ let notebookUrl = args.notebook_url;
1433
+ if (!notebookUrl && args.notebook_id) {
1434
+ const notebook = this.library.getNotebook(args.notebook_id);
1435
+ if (!notebook) {
1436
+ throw new Error(`Notebook not found in library: ${args.notebook_id}`);
1437
+ }
1438
+ notebookUrl = notebook.url;
1439
+ log.info(` Resolved notebook: ${notebook.name}`);
1440
+ }
1441
+ else if (!notebookUrl) {
1442
+ const active = this.library.getActiveNotebook();
1443
+ if (active) {
1444
+ notebookUrl = active.url;
1445
+ log.info(` Using active notebook: ${active.name}`);
1446
+ }
1447
+ else {
1448
+ throw new Error("No notebook specified. Provide notebook_id or notebook_url.");
1449
+ }
1450
+ }
1451
+ // Validate URL
1452
+ const safeUrl = validateNotebookUrl(notebookUrl);
1453
+ // Get the shared context manager from session manager
1454
+ const contextManager = this.sessionManager.getContextManager();
1455
+ // Generate audio
1456
+ const audioManager = new AudioManager(this.authManager, contextManager);
1457
+ const result = await audioManager.generateAudioOverview(safeUrl);
1458
+ if (result.success) {
1459
+ log.success(`✅ [TOOL] generate_audio_overview completed (status: ${result.status.status})`);
1460
+ }
1461
+ else {
1462
+ log.warning(`⚠️ [TOOL] generate_audio_overview: ${result.error}`);
1463
+ }
1464
+ return {
1465
+ success: result.success,
1466
+ data: result,
1467
+ ...(result.error && { error: result.error }),
1468
+ };
1469
+ }
1470
+ catch (error) {
1471
+ const errorMessage = error instanceof Error ? error.message : String(error);
1472
+ log.error(`❌ [TOOL] generate_audio_overview failed: ${errorMessage}`);
1473
+ return {
1474
+ success: false,
1475
+ error: errorMessage,
1476
+ };
1477
+ }
1478
+ }
1479
+ /**
1480
+ * Handle get_audio_status tool
1481
+ *
1482
+ * Checks the audio generation status for a notebook.
1483
+ */
1484
+ async handleGetAudioStatus(args) {
1485
+ log.info(`🔧 [TOOL] get_audio_status called`);
1486
+ try {
1487
+ // Resolve notebook URL
1488
+ let notebookUrl = args.notebook_url;
1489
+ if (!notebookUrl && args.notebook_id) {
1490
+ const notebook = this.library.getNotebook(args.notebook_id);
1491
+ if (!notebook) {
1492
+ throw new Error(`Notebook not found in library: ${args.notebook_id}`);
1493
+ }
1494
+ notebookUrl = notebook.url;
1495
+ log.info(` Resolved notebook: ${notebook.name}`);
1496
+ }
1497
+ else if (!notebookUrl) {
1498
+ const active = this.library.getActiveNotebook();
1499
+ if (active) {
1500
+ notebookUrl = active.url;
1501
+ log.info(` Using active notebook: ${active.name}`);
1502
+ }
1503
+ else {
1504
+ throw new Error("No notebook specified. Provide notebook_id or notebook_url.");
1505
+ }
1506
+ }
1507
+ // Validate URL
1508
+ const safeUrl = validateNotebookUrl(notebookUrl);
1509
+ // Get the shared context manager from session manager
1510
+ const contextManager = this.sessionManager.getContextManager();
1511
+ // Get status
1512
+ const audioManager = new AudioManager(this.authManager, contextManager);
1513
+ const status = await audioManager.getAudioStatus(safeUrl);
1514
+ log.success(`✅ [TOOL] get_audio_status completed (status: ${status.status})`);
1515
+ return {
1516
+ success: true,
1517
+ data: status,
1518
+ };
1519
+ }
1520
+ catch (error) {
1521
+ const errorMessage = error instanceof Error ? error.message : String(error);
1522
+ log.error(`❌ [TOOL] get_audio_status failed: ${errorMessage}`);
1523
+ return {
1524
+ success: false,
1525
+ error: errorMessage,
1526
+ };
1527
+ }
1528
+ }
1529
+ /**
1530
+ * Handle download_audio tool
1531
+ *
1532
+ * Downloads the generated audio file.
1533
+ */
1534
+ async handleDownloadAudio(args) {
1535
+ log.info(`🔧 [TOOL] download_audio called`);
1536
+ try {
1537
+ // Resolve notebook URL
1538
+ let notebookUrl = args.notebook_url;
1539
+ if (!notebookUrl && args.notebook_id) {
1540
+ const notebook = this.library.getNotebook(args.notebook_id);
1541
+ if (!notebook) {
1542
+ throw new Error(`Notebook not found in library: ${args.notebook_id}`);
1543
+ }
1544
+ notebookUrl = notebook.url;
1545
+ log.info(` Resolved notebook: ${notebook.name}`);
1546
+ }
1547
+ else if (!notebookUrl) {
1548
+ const active = this.library.getActiveNotebook();
1549
+ if (active) {
1550
+ notebookUrl = active.url;
1551
+ log.info(` Using active notebook: ${active.name}`);
1552
+ }
1553
+ else {
1554
+ throw new Error("No notebook specified. Provide notebook_id or notebook_url.");
1555
+ }
1556
+ }
1557
+ // Validate URL
1558
+ const safeUrl = validateNotebookUrl(notebookUrl);
1559
+ // Get the shared context manager from session manager
1560
+ const contextManager = this.sessionManager.getContextManager();
1561
+ // Download audio
1562
+ const audioManager = new AudioManager(this.authManager, contextManager);
1563
+ const result = await audioManager.downloadAudio(safeUrl, args.output_path);
1564
+ if (result.success) {
1565
+ log.success(`✅ [TOOL] download_audio completed: ${result.filePath}`);
1566
+ }
1567
+ else {
1568
+ log.warning(`⚠️ [TOOL] download_audio: ${result.error}`);
1569
+ }
1570
+ return {
1571
+ success: result.success,
1572
+ data: result,
1573
+ ...(result.error && { error: result.error }),
1574
+ };
1575
+ }
1576
+ catch (error) {
1577
+ const errorMessage = error instanceof Error ? error.message : String(error);
1578
+ log.error(`❌ [TOOL] download_audio failed: ${errorMessage}`);
1579
+ return {
1580
+ success: false,
1581
+ error: errorMessage,
1582
+ };
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Handle configure_webhook tool
1587
+ *
1588
+ * Add or update a webhook endpoint.
1589
+ */
1590
+ async handleConfigureWebhook(args) {
1591
+ log.info(`🔧 [TOOL] configure_webhook called`);
1592
+ log.info(` Name: ${args.name}`);
1593
+ try {
1594
+ const dispatcher = getWebhookDispatcher();
1595
+ if (args.id) {
1596
+ // Update existing
1597
+ const updated = dispatcher.updateWebhook({
1598
+ id: args.id,
1599
+ name: args.name,
1600
+ url: args.url,
1601
+ enabled: args.enabled,
1602
+ events: args.events,
1603
+ format: args.format,
1604
+ secret: args.secret,
1605
+ });
1606
+ if (!updated) {
1607
+ throw new Error(`Webhook not found: ${args.id}`);
1608
+ }
1609
+ log.success(`✅ [TOOL] configure_webhook updated: ${updated.name}`);
1610
+ return { success: true, data: updated };
1611
+ }
1612
+ else {
1613
+ // Create new
1614
+ const webhook = dispatcher.addWebhook({
1615
+ name: args.name,
1616
+ url: args.url,
1617
+ events: args.events,
1618
+ format: args.format,
1619
+ secret: args.secret,
1620
+ });
1621
+ log.success(`✅ [TOOL] configure_webhook created: ${webhook.name}`);
1622
+ return { success: true, data: webhook };
1623
+ }
1624
+ }
1625
+ catch (error) {
1626
+ const errorMessage = error instanceof Error ? error.message : String(error);
1627
+ log.error(`❌ [TOOL] configure_webhook failed: ${errorMessage}`);
1628
+ return { success: false, error: errorMessage };
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Handle list_webhooks tool
1633
+ *
1634
+ * List all configured webhooks.
1635
+ */
1636
+ async handleListWebhooks() {
1637
+ log.info(`🔧 [TOOL] list_webhooks called`);
1638
+ try {
1639
+ const dispatcher = getWebhookDispatcher();
1640
+ const webhooks = dispatcher.listWebhooks();
1641
+ const stats = dispatcher.getStats();
1642
+ log.success(`✅ [TOOL] list_webhooks completed (${webhooks.length} webhooks)`);
1643
+ return {
1644
+ success: true,
1645
+ data: { webhooks, stats },
1646
+ };
1647
+ }
1648
+ catch (error) {
1649
+ const errorMessage = error instanceof Error ? error.message : String(error);
1650
+ log.error(`❌ [TOOL] list_webhooks failed: ${errorMessage}`);
1651
+ return { success: false, error: errorMessage };
1652
+ }
1653
+ }
1654
+ /**
1655
+ * Handle test_webhook tool
1656
+ *
1657
+ * Send a test event to a webhook.
1658
+ */
1659
+ async handleTestWebhook(args) {
1660
+ log.info(`🔧 [TOOL] test_webhook called`);
1661
+ log.info(` ID: ${args.id}`);
1662
+ try {
1663
+ const dispatcher = getWebhookDispatcher();
1664
+ const result = await dispatcher.testWebhook(args.id);
1665
+ if (result.success) {
1666
+ log.success(`✅ [TOOL] test_webhook succeeded`);
1667
+ return {
1668
+ success: true,
1669
+ data: { success: true, message: "Test event delivered successfully" },
1670
+ };
1671
+ }
1672
+ else {
1673
+ log.warning(`⚠️ [TOOL] test_webhook failed: ${result.error}`);
1674
+ return {
1675
+ success: false,
1676
+ data: { success: false, message: result.error || "Test failed" },
1677
+ error: result.error,
1678
+ };
1679
+ }
1680
+ }
1681
+ catch (error) {
1682
+ const errorMessage = error instanceof Error ? error.message : String(error);
1683
+ log.error(`❌ [TOOL] test_webhook failed: ${errorMessage}`);
1684
+ return { success: false, error: errorMessage };
1685
+ }
1686
+ }
1687
+ /**
1688
+ * Handle remove_webhook tool
1689
+ *
1690
+ * Remove a configured webhook.
1691
+ */
1692
+ async handleRemoveWebhook(args) {
1693
+ log.info(`🔧 [TOOL] remove_webhook called`);
1694
+ log.info(` ID: ${args.id}`);
1695
+ try {
1696
+ const dispatcher = getWebhookDispatcher();
1697
+ const removed = dispatcher.removeWebhook(args.id);
1698
+ if (removed) {
1699
+ log.success(`✅ [TOOL] remove_webhook completed`);
1700
+ return {
1701
+ success: true,
1702
+ data: { removed: true, id: args.id },
1703
+ };
1704
+ }
1705
+ else {
1706
+ log.warning(`⚠️ [TOOL] Webhook not found: ${args.id}`);
1707
+ return {
1708
+ success: false,
1709
+ error: `Webhook not found: ${args.id}`,
1710
+ };
1711
+ }
1712
+ }
1713
+ catch (error) {
1714
+ const errorMessage = error instanceof Error ? error.message : String(error);
1715
+ log.error(`❌ [TOOL] remove_webhook failed: ${errorMessage}`);
1716
+ return { success: false, error: errorMessage };
1717
+ }
1718
+ }
1719
+ // ==================== GEMINI API HANDLERS ====================
1720
+ /**
1721
+ * Handle deep_research tool
1722
+ *
1723
+ * Performs comprehensive research using Gemini's Deep Research agent.
1724
+ */
1725
+ async handleDeepResearch(args, sendProgress) {
1726
+ const startTime = Date.now();
1727
+ log.info(`🔧 [TOOL] deep_research called`);
1728
+ log.info(` Query: "${sanitizeForLogging(args.query.substring(0, 100))}"...`);
1729
+ // Check if Gemini is available
1730
+ if (!this.geminiClient.isAvailable()) {
1731
+ log.error(`❌ [TOOL] deep_research failed: Gemini API key not configured`);
1732
+ return {
1733
+ success: false,
1734
+ error: "Gemini API key not configured. Set GEMINI_API_KEY environment variable.",
1735
+ };
1736
+ }
1737
+ try {
1738
+ // Validate query
1739
+ if (!args.query || args.query.trim().length === 0) {
1740
+ throw new Error("Query cannot be empty");
1741
+ }
1742
+ if (args.query.length > 10000) {
1743
+ throw new Error("Query too long (max 10000 characters)");
1744
+ }
1745
+ // Validate max_wait_seconds
1746
+ const maxWaitSeconds = Math.min(args.max_wait_seconds || 300, 600); // Max 10 minutes
1747
+ const maxWaitMs = maxWaitSeconds * 1000;
1748
+ if (sendProgress) {
1749
+ await sendProgress("Starting deep research...", 0, 100);
1750
+ }
1751
+ // Start the research
1752
+ const interaction = await this.geminiClient.deepResearch({
1753
+ query: args.query,
1754
+ background: true,
1755
+ waitForCompletion: args.wait_for_completion !== false,
1756
+ maxWaitMs,
1757
+ progressCallback: sendProgress,
1758
+ });
1759
+ const durationMs = Date.now() - startTime;
1760
+ // Extract the answer
1761
+ const answer = interaction.outputs.find(o => o.type === "text")?.text || "";
1762
+ // Audit log
1763
+ await audit.tool("deep_research", { query: sanitizeForLogging(args.query) }, true, durationMs);
1764
+ log.success(`✅ [TOOL] deep_research completed in ${durationMs}ms`);
1765
+ return {
1766
+ success: true,
1767
+ data: {
1768
+ interactionId: interaction.id,
1769
+ status: interaction.status,
1770
+ answer,
1771
+ tokensUsed: interaction.usage?.totalTokens,
1772
+ durationMs,
1773
+ },
1774
+ };
1775
+ }
1776
+ catch (error) {
1777
+ const errorMessage = error instanceof Error ? error.message : String(error);
1778
+ const durationMs = Date.now() - startTime;
1779
+ log.error(`❌ [TOOL] deep_research failed: ${errorMessage}`);
1780
+ await audit.tool("deep_research", { query: sanitizeForLogging(args.query) }, false, durationMs, errorMessage);
1781
+ return { success: false, error: errorMessage };
1782
+ }
1783
+ }
1784
+ /**
1785
+ * Handle gemini_query tool
1786
+ *
1787
+ * Quick query to Gemini model with optional grounding tools.
1788
+ */
1789
+ async handleGeminiQuery(args) {
1790
+ const startTime = Date.now();
1791
+ log.info(`🔧 [TOOL] gemini_query called`);
1792
+ log.info(` Query: "${sanitizeForLogging(args.query.substring(0, 100))}"...`);
1793
+ log.info(` Model: ${args.model || "default"}`);
1794
+ if (args.tools)
1795
+ log.info(` Tools: ${args.tools.join(", ")}`);
1796
+ // Check if Gemini is available
1797
+ if (!this.geminiClient.isAvailable()) {
1798
+ log.error(`❌ [TOOL] gemini_query failed: Gemini API key not configured`);
1799
+ return {
1800
+ success: false,
1801
+ error: "Gemini API key not configured. Set GEMINI_API_KEY environment variable.",
1802
+ };
1803
+ }
1804
+ try {
1805
+ // Validate query
1806
+ if (!args.query || args.query.trim().length === 0) {
1807
+ throw new Error("Query cannot be empty");
1808
+ }
1809
+ if (args.query.length > 30000) {
1810
+ throw new Error("Query too long (max 30000 characters)");
1811
+ }
1812
+ // If URLs provided, auto-enable url_context
1813
+ let tools = args.tools || [];
1814
+ if (args.urls && args.urls.length > 0 && !tools.includes("url_context")) {
1815
+ tools = [...tools, "url_context"];
1816
+ }
1817
+ // Validate URLs if provided
1818
+ if (args.urls) {
1819
+ for (const url of args.urls) {
1820
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
1821
+ throw new Error(`Invalid URL: ${url} (must start with http:// or https://)`);
1822
+ }
1823
+ }
1824
+ }
1825
+ const interaction = await this.geminiClient.query({
1826
+ query: args.query,
1827
+ model: args.model,
1828
+ tools,
1829
+ urls: args.urls,
1830
+ previousInteractionId: args.previous_interaction_id,
1831
+ });
1832
+ const durationMs = Date.now() - startTime;
1833
+ // Extract the answer
1834
+ const answer = interaction.outputs.find(o => o.type === "text")?.text || "";
1835
+ // Identify which tools were used
1836
+ const toolsUsed = interaction.outputs
1837
+ .filter(o => o.type === "function_call")
1838
+ .map(o => o.name)
1839
+ .filter((name) => !!name);
1840
+ // Audit log
1841
+ await audit.tool("gemini_query", {
1842
+ query: sanitizeForLogging(args.query),
1843
+ model: args.model,
1844
+ tools: args.tools,
1845
+ }, true, durationMs);
1846
+ log.success(`✅ [TOOL] gemini_query completed in ${durationMs}ms`);
1847
+ return {
1848
+ success: true,
1849
+ data: {
1850
+ interactionId: interaction.id,
1851
+ answer,
1852
+ model: interaction.model || args.model || CONFIG.geminiDefaultModel,
1853
+ tokensUsed: interaction.usage?.totalTokens,
1854
+ toolsUsed: toolsUsed.length > 0 ? toolsUsed : undefined,
1855
+ },
1856
+ };
1857
+ }
1858
+ catch (error) {
1859
+ const errorMessage = error instanceof Error ? error.message : String(error);
1860
+ const durationMs = Date.now() - startTime;
1861
+ log.error(`❌ [TOOL] gemini_query failed: ${errorMessage}`);
1862
+ await audit.tool("gemini_query", { query: sanitizeForLogging(args.query) }, false, durationMs, errorMessage);
1863
+ return { success: false, error: errorMessage };
1864
+ }
1865
+ }
1866
+ /**
1867
+ * Handle get_research_status tool
1868
+ *
1869
+ * Check the status of a background deep research task.
1870
+ */
1871
+ async handleGetResearchStatus(args) {
1872
+ log.info(`🔧 [TOOL] get_research_status called`);
1873
+ log.info(` Interaction ID: ${args.interaction_id}`);
1874
+ // Check if Gemini is available
1875
+ if (!this.geminiClient.isAvailable()) {
1876
+ log.error(`❌ [TOOL] get_research_status failed: Gemini API key not configured`);
1877
+ return {
1878
+ success: false,
1879
+ error: "Gemini API key not configured. Set GEMINI_API_KEY environment variable.",
1880
+ };
1881
+ }
1882
+ try {
1883
+ // Validate interaction_id
1884
+ if (!args.interaction_id || args.interaction_id.trim().length === 0) {
1885
+ throw new Error("Interaction ID cannot be empty");
1886
+ }
1887
+ const interaction = await this.geminiClient.getInteraction(args.interaction_id);
1888
+ log.success(`✅ [TOOL] get_research_status: ${interaction.status}`);
1889
+ return {
1890
+ success: true,
1891
+ data: interaction,
1892
+ };
1893
+ }
1894
+ catch (error) {
1895
+ const errorMessage = error instanceof Error ? error.message : String(error);
1896
+ log.error(`❌ [TOOL] get_research_status failed: ${errorMessage}`);
1897
+ return { success: false, error: errorMessage };
1898
+ }
1899
+ }
1900
+ // ==================== CLEANUP ====================
804
1901
  /**
805
1902
  * Cleanup all resources (called on server shutdown)
806
1903
  */