libre-webui 0.2.4

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 (233) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +204 -0
  3. package/backend/dist/db.d.ts +19 -0
  4. package/backend/dist/db.d.ts.map +1 -0
  5. package/backend/dist/db.js +355 -0
  6. package/backend/dist/db.js.map +1 -0
  7. package/backend/dist/env.d.ts +2 -0
  8. package/backend/dist/env.d.ts.map +1 -0
  9. package/backend/dist/env.js +22 -0
  10. package/backend/dist/env.js.map +1 -0
  11. package/backend/dist/index.d.ts +4 -0
  12. package/backend/dist/index.d.ts.map +1 -0
  13. package/backend/dist/index.js +751 -0
  14. package/backend/dist/index.js.map +1 -0
  15. package/backend/dist/middleware/auth.d.ts +18 -0
  16. package/backend/dist/middleware/auth.d.ts.map +1 -0
  17. package/backend/dist/middleware/auth.js +98 -0
  18. package/backend/dist/middleware/auth.js.map +1 -0
  19. package/backend/dist/middleware/index.d.ts +5 -0
  20. package/backend/dist/middleware/index.d.ts.map +1 -0
  21. package/backend/dist/middleware/index.js +62 -0
  22. package/backend/dist/middleware/index.js.map +1 -0
  23. package/backend/dist/models/personaModel.d.ts +37 -0
  24. package/backend/dist/models/personaModel.d.ts.map +1 -0
  25. package/backend/dist/models/personaModel.js +269 -0
  26. package/backend/dist/models/personaModel.js.map +1 -0
  27. package/backend/dist/models/userModel.d.ts +86 -0
  28. package/backend/dist/models/userModel.d.ts.map +1 -0
  29. package/backend/dist/models/userModel.js +212 -0
  30. package/backend/dist/models/userModel.js.map +1 -0
  31. package/backend/dist/routes/auth.d.ts +3 -0
  32. package/backend/dist/routes/auth.d.ts.map +1 -0
  33. package/backend/dist/routes/auth.js +389 -0
  34. package/backend/dist/routes/auth.js.map +1 -0
  35. package/backend/dist/routes/chat.d.ts +3 -0
  36. package/backend/dist/routes/chat.d.ts.map +1 -0
  37. package/backend/dist/routes/chat.js +767 -0
  38. package/backend/dist/routes/chat.js.map +1 -0
  39. package/backend/dist/routes/documents.d.ts +3 -0
  40. package/backend/dist/routes/documents.d.ts.map +1 -0
  41. package/backend/dist/routes/documents.js +244 -0
  42. package/backend/dist/routes/documents.js.map +1 -0
  43. package/backend/dist/routes/ollama.d.ts +3 -0
  44. package/backend/dist/routes/ollama.d.ts.map +1 -0
  45. package/backend/dist/routes/ollama.js +549 -0
  46. package/backend/dist/routes/ollama.js.map +1 -0
  47. package/backend/dist/routes/personas.d.ts +3 -0
  48. package/backend/dist/routes/personas.d.ts.map +1 -0
  49. package/backend/dist/routes/personas.js +505 -0
  50. package/backend/dist/routes/personas.js.map +1 -0
  51. package/backend/dist/routes/plugins.d.ts +3 -0
  52. package/backend/dist/routes/plugins.d.ts.map +1 -0
  53. package/backend/dist/routes/plugins.js +417 -0
  54. package/backend/dist/routes/plugins.js.map +1 -0
  55. package/backend/dist/routes/preferences.d.ts +3 -0
  56. package/backend/dist/routes/preferences.d.ts.map +1 -0
  57. package/backend/dist/routes/preferences.js +303 -0
  58. package/backend/dist/routes/preferences.js.map +1 -0
  59. package/backend/dist/routes/tts.d.ts +3 -0
  60. package/backend/dist/routes/tts.d.ts.map +1 -0
  61. package/backend/dist/routes/tts.js +304 -0
  62. package/backend/dist/routes/tts.js.map +1 -0
  63. package/backend/dist/routes/users.d.ts +3 -0
  64. package/backend/dist/routes/users.d.ts.map +1 -0
  65. package/backend/dist/routes/users.js +246 -0
  66. package/backend/dist/routes/users.js.map +1 -0
  67. package/backend/dist/services/authService.d.ts +51 -0
  68. package/backend/dist/services/authService.d.ts.map +1 -0
  69. package/backend/dist/services/authService.js +153 -0
  70. package/backend/dist/services/authService.js.map +1 -0
  71. package/backend/dist/services/chatService.d.ts +52 -0
  72. package/backend/dist/services/chatService.d.ts.map +1 -0
  73. package/backend/dist/services/chatService.js +645 -0
  74. package/backend/dist/services/chatService.js.map +1 -0
  75. package/backend/dist/services/documentService.d.ts +34 -0
  76. package/backend/dist/services/documentService.d.ts.map +1 -0
  77. package/backend/dist/services/documentService.js +428 -0
  78. package/backend/dist/services/documentService.js.map +1 -0
  79. package/backend/dist/services/encryptionService.d.ts +62 -0
  80. package/backend/dist/services/encryptionService.d.ts.map +1 -0
  81. package/backend/dist/services/encryptionService.js +284 -0
  82. package/backend/dist/services/encryptionService.js.map +1 -0
  83. package/backend/dist/services/memoryService.d.ts +140 -0
  84. package/backend/dist/services/memoryService.d.ts.map +1 -0
  85. package/backend/dist/services/memoryService.js +867 -0
  86. package/backend/dist/services/memoryService.js.map +1 -0
  87. package/backend/dist/services/mutationEngineService.d.ts +49 -0
  88. package/backend/dist/services/mutationEngineService.d.ts.map +1 -0
  89. package/backend/dist/services/mutationEngineService.js +432 -0
  90. package/backend/dist/services/mutationEngineService.js.map +1 -0
  91. package/backend/dist/services/ollamaService.d.ts +55 -0
  92. package/backend/dist/services/ollamaService.d.ts.map +1 -0
  93. package/backend/dist/services/ollamaService.js +450 -0
  94. package/backend/dist/services/ollamaService.js.map +1 -0
  95. package/backend/dist/services/personaService.d.ts +67 -0
  96. package/backend/dist/services/personaService.d.ts.map +1 -0
  97. package/backend/dist/services/personaService.js +373 -0
  98. package/backend/dist/services/personaService.js.map +1 -0
  99. package/backend/dist/services/pluginService.d.ts +42 -0
  100. package/backend/dist/services/pluginService.d.ts.map +1 -0
  101. package/backend/dist/services/pluginService.js +961 -0
  102. package/backend/dist/services/pluginService.js.map +1 -0
  103. package/backend/dist/services/preferencesService.d.ts +35 -0
  104. package/backend/dist/services/preferencesService.d.ts.map +1 -0
  105. package/backend/dist/services/preferencesService.js +255 -0
  106. package/backend/dist/services/preferencesService.js.map +1 -0
  107. package/backend/dist/services/simpleGitHubOAuth.d.ts +48 -0
  108. package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -0
  109. package/backend/dist/services/simpleGitHubOAuth.js +203 -0
  110. package/backend/dist/services/simpleGitHubOAuth.js.map +1 -0
  111. package/backend/dist/services/simpleHuggingFaceOAuth.d.ts +43 -0
  112. package/backend/dist/services/simpleHuggingFaceOAuth.d.ts.map +1 -0
  113. package/backend/dist/services/simpleHuggingFaceOAuth.js +159 -0
  114. package/backend/dist/services/simpleHuggingFaceOAuth.js.map +1 -0
  115. package/backend/dist/services/userService.d.ts +1 -0
  116. package/backend/dist/services/userService.d.ts.map +1 -0
  117. package/backend/dist/services/userService.js +18 -0
  118. package/backend/dist/services/userService.js.map +1 -0
  119. package/backend/dist/storage.d.ts +55 -0
  120. package/backend/dist/storage.d.ts.map +1 -0
  121. package/backend/dist/storage.js +741 -0
  122. package/backend/dist/storage.js.map +1 -0
  123. package/backend/dist/test-encryption.d.ts +2 -0
  124. package/backend/dist/test-encryption.d.ts.map +1 -0
  125. package/backend/dist/test-encryption.js +64 -0
  126. package/backend/dist/test-encryption.js.map +1 -0
  127. package/backend/dist/types/index.d.ts +523 -0
  128. package/backend/dist/types/index.d.ts.map +1 -0
  129. package/backend/dist/types/index.js +31 -0
  130. package/backend/dist/types/index.js.map +1 -0
  131. package/backend/dist/utils/generationUtils.d.ts +10 -0
  132. package/backend/dist/utils/generationUtils.d.ts.map +1 -0
  133. package/backend/dist/utils/generationUtils.js +49 -0
  134. package/backend/dist/utils/generationUtils.js.map +1 -0
  135. package/backend/dist/utils/hash.d.ts +29 -0
  136. package/backend/dist/utils/hash.d.ts.map +1 -0
  137. package/backend/dist/utils/hash.js +73 -0
  138. package/backend/dist/utils/hash.js.map +1 -0
  139. package/backend/dist/utils/jwt.d.ts +37 -0
  140. package/backend/dist/utils/jwt.d.ts.map +1 -0
  141. package/backend/dist/utils/jwt.js +86 -0
  142. package/backend/dist/utils/jwt.js.map +1 -0
  143. package/bin/cli.js +150 -0
  144. package/electron/main.js +322 -0
  145. package/frontend/dist/_redirects +1 -0
  146. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  147. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  148. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  149. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  150. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  151. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  152. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  153. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  154. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  155. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  156. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  157. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  158. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  159. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  160. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  161. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  162. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  163. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  164. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  165. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  166. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  167. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  168. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  169. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  170. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  171. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  172. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  173. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  174. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  175. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  176. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  177. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  178. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  179. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  180. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  181. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  182. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  183. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  184. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  185. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  186. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  187. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  188. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  189. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  190. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  191. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  192. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  193. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  194. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  195. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  196. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  197. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  198. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  199. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  200. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  201. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  202. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  203. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  204. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  205. package/frontend/dist/assets/index-CRQkB7Wz.js +3 -0
  206. package/frontend/dist/css/index-B1OjddR-.css +1 -0
  207. package/frontend/dist/favicon-dark.png +0 -0
  208. package/frontend/dist/favicon-light.png +0 -0
  209. package/frontend/dist/index.html +23 -0
  210. package/frontend/dist/js/ArtifactContainer-c_oi7XMs.js +23 -0
  211. package/frontend/dist/js/ArtifactDemoPage-CdfwJVXu.js +98 -0
  212. package/frontend/dist/js/ChatPage-CyotkmS0.js +281 -0
  213. package/frontend/dist/js/ModelsPage-DNaziPHc.js +2 -0
  214. package/frontend/dist/js/PersonasPage-DcnbJf8Q.js +13 -0
  215. package/frontend/dist/js/UserManagementPage-DtTf92dS.js +1 -0
  216. package/frontend/dist/js/markdown-vendor-D-79K2xZ.js +22 -0
  217. package/frontend/dist/js/react-vendor-N--QU9DW.js +8 -0
  218. package/frontend/dist/js/router-vendor-B-t91v39.js +3 -0
  219. package/frontend/dist/js/ui-vendor-VxSCY_bv.js +177 -0
  220. package/frontend/dist/js/utils-vendor-DNzxLBGx.js +6 -0
  221. package/frontend/dist/logo-dark.png +0 -0
  222. package/frontend/dist/logo-light.png +0 -0
  223. package/frontend/dist/logo.svg +14 -0
  224. package/package.json +128 -0
  225. package/plugins/anthropic.json +25 -0
  226. package/plugins/elevenlabs.json +58 -0
  227. package/plugins/gemini.json +57 -0
  228. package/plugins/github.json +23 -0
  229. package/plugins/groq.json +25 -0
  230. package/plugins/mistral.json +73 -0
  231. package/plugins/openai-tts.json +38 -0
  232. package/plugins/openai.json +132 -0
  233. package/plugins/openrouter.json +353 -0
@@ -0,0 +1,961 @@
1
+ /*
2
+ * Libre WebUI
3
+ * Copyright (C) 2025 Kroonen AI, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import sanitize from 'sanitize-filename';
20
+ import axios from 'axios';
21
+ class PluginService {
22
+ constructor() {
23
+ this.activePluginIds = new Set();
24
+ this.pluginsDir = path.join(process.cwd(), 'plugins');
25
+ this.ensurePluginsDirectory();
26
+ this.loadActivePlugins();
27
+ }
28
+ ensurePluginsDirectory() {
29
+ if (!fs.existsSync(this.pluginsDir)) {
30
+ fs.mkdirSync(this.pluginsDir, { recursive: true });
31
+ }
32
+ }
33
+ loadActivePlugins() {
34
+ const statusFile = path.join(this.pluginsDir, '.status.json');
35
+ if (fs.existsSync(statusFile)) {
36
+ try {
37
+ const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
38
+ if (Array.isArray(status.activePlugins)) {
39
+ this.activePluginIds = new Set(status.activePlugins);
40
+ }
41
+ else if (status.activePlugin) {
42
+ // Legacy support for single active plugin
43
+ this.activePluginIds = new Set([status.activePlugin]);
44
+ }
45
+ }
46
+ catch (error) {
47
+ console.error('Failed to load plugin status:', error);
48
+ }
49
+ }
50
+ }
51
+ saveActivePlugins() {
52
+ const statusFile = path.join(this.pluginsDir, '.status.json');
53
+ const status = {
54
+ activePlugins: Array.from(this.activePluginIds),
55
+ lastUpdated: new Date().toISOString(),
56
+ };
57
+ fs.writeFileSync(statusFile, JSON.stringify(status, null, 2));
58
+ }
59
+ // List all installed plugins
60
+ getAllPlugins() {
61
+ const plugins = [];
62
+ try {
63
+ const files = fs.readdirSync(this.pluginsDir);
64
+ for (const file of files) {
65
+ if (file.endsWith('.json') && !file.startsWith('.')) {
66
+ try {
67
+ const filePath = path.join(this.pluginsDir, file);
68
+ const content = fs.readFileSync(filePath, 'utf8');
69
+ const plugin = JSON.parse(content);
70
+ // Validate plugin structure
71
+ if (this.validatePlugin(plugin)) {
72
+ plugin.active = this.activePluginIds.has(plugin.id);
73
+ plugins.push(plugin);
74
+ }
75
+ }
76
+ catch (error) {
77
+ console.error(`Failed to load plugin ${file}:`, error);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ catch (error) {
83
+ console.error('Failed to read plugins directory:', error);
84
+ }
85
+ return plugins;
86
+ }
87
+ // Get a specific plugin by ID
88
+ getPlugin(id) {
89
+ // Sanitize the ID to prevent path traversal
90
+ const sanitizedId = sanitize(id);
91
+ if (!sanitizedId || sanitizedId !== id) {
92
+ console.error('Invalid plugin ID provided:', id);
93
+ return null;
94
+ }
95
+ const filePath = path.resolve(this.pluginsDir, `${sanitizedId}.json`);
96
+ // Ensure the file path is within the plugins directory
97
+ if (!filePath.startsWith(path.resolve(this.pluginsDir)) ||
98
+ !fs.existsSync(filePath)) {
99
+ return null;
100
+ }
101
+ try {
102
+ const content = fs.readFileSync(filePath, 'utf8');
103
+ const plugin = JSON.parse(content);
104
+ if (this.validatePlugin(plugin)) {
105
+ plugin.active = this.activePluginIds.has(plugin.id);
106
+ return plugin;
107
+ }
108
+ }
109
+ catch (error) {
110
+ console.error('Failed to load plugin %s:', sanitizedId, error);
111
+ }
112
+ return null;
113
+ }
114
+ // Install or update a plugin
115
+ installPlugin(pluginData) {
116
+ if (!this.validatePlugin(pluginData)) {
117
+ throw new Error('Invalid plugin structure');
118
+ }
119
+ const now = Date.now();
120
+ const plugin = {
121
+ ...pluginData,
122
+ created_at: pluginData.created_at || now,
123
+ updated_at: now,
124
+ active: false,
125
+ };
126
+ const filePath = path.join(this.pluginsDir, `${plugin.id}.json`);
127
+ fs.writeFileSync(filePath, JSON.stringify(plugin, null, 2));
128
+ return plugin;
129
+ }
130
+ // Delete a plugin
131
+ deletePlugin(id) {
132
+ // Validate the ID parameter using a strict pattern
133
+ const idPattern = /^[a-zA-Z0-9_-]+$/;
134
+ if (!idPattern.test(id)) {
135
+ console.error('Invalid plugin ID format:', id);
136
+ return false;
137
+ }
138
+ // Sanitize the ID to prevent path traversal
139
+ const sanitizedId = sanitize(id);
140
+ if (!sanitizedId || sanitizedId !== id) {
141
+ console.error('Plugin ID failed sanitization:', id);
142
+ return false;
143
+ }
144
+ const filePath = path.resolve(this.pluginsDir, `${sanitizedId}.json`);
145
+ // Ensure the file path is within the plugins directory
146
+ if (!filePath.startsWith(path.resolve(this.pluginsDir)) ||
147
+ !fs.existsSync(filePath)) {
148
+ console.error('File path is invalid or does not exist:', filePath);
149
+ return false;
150
+ }
151
+ try {
152
+ fs.unlinkSync(filePath);
153
+ // If this was an active plugin, deactivate it
154
+ if (this.activePluginIds.has(id)) {
155
+ this.activePluginIds.delete(id);
156
+ this.saveActivePlugins();
157
+ }
158
+ return true;
159
+ }
160
+ catch (error) {
161
+ console.error('Failed to delete plugin %s:', sanitizedId, error);
162
+ return false;
163
+ }
164
+ }
165
+ // Activate a plugin
166
+ activatePlugin(id) {
167
+ const plugin = this.getPlugin(id);
168
+ if (!plugin) {
169
+ throw new Error('Plugin not found');
170
+ }
171
+ this.activePluginIds.add(id);
172
+ this.saveActivePlugins();
173
+ return true;
174
+ }
175
+ // Deactivate a specific plugin
176
+ deactivatePlugin(id) {
177
+ if (id) {
178
+ this.activePluginIds.delete(id);
179
+ }
180
+ else {
181
+ // Legacy: deactivate all plugins
182
+ this.activePluginIds.clear();
183
+ }
184
+ this.saveActivePlugins();
185
+ return true;
186
+ }
187
+ // Get the active plugin for a specific model
188
+ getActivePluginForModel(model) {
189
+ console.log(`[DEBUG] Looking for plugin for model: ${model}`);
190
+ // Get all available plugins (not just active ones)
191
+ const allPlugins = this.getAllPlugins();
192
+ console.log(`[DEBUG] Available plugins:`, allPlugins.map(p => p.id));
193
+ // Find the plugin that supports this model
194
+ for (const plugin of allPlugins) {
195
+ console.log(`[DEBUG] Checking plugin ${plugin.id} with model_map:`, plugin.model_map);
196
+ if (plugin.model_map.includes(model)) {
197
+ console.log(`[DEBUG] Found plugin ${plugin.id} for model ${model}`);
198
+ // Check if we have the required API key
199
+ const apiKey = process.env[plugin.auth.key_env];
200
+ if (!apiKey) {
201
+ console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env} not set`);
202
+ continue;
203
+ }
204
+ return plugin;
205
+ }
206
+ }
207
+ console.log(`[DEBUG] No plugin found for model: ${model}`);
208
+ return null;
209
+ }
210
+ // Get all currently active plugins
211
+ getActivePlugins() {
212
+ const allPlugins = this.getAllPlugins();
213
+ console.log('All plugins:', allPlugins.map(p => ({ id: p.id, active: p.active })));
214
+ console.log('Active plugin IDs in memory:', Array.from(this.activePluginIds));
215
+ const activePlugins = allPlugins.filter(plugin => this.activePluginIds.has(plugin.id));
216
+ console.log('Filtered active plugins:', activePlugins.map(p => p.id));
217
+ return activePlugins;
218
+ }
219
+ // Legacy method for backward compatibility - returns first active plugin
220
+ getActivePlugin() {
221
+ const activePlugins = this.getActivePlugins();
222
+ return activePlugins.length > 0 ? activePlugins[0] : null;
223
+ }
224
+ // Get plugin status
225
+ getPluginStatus() {
226
+ const plugins = this.getAllPlugins();
227
+ return plugins.map(plugin => ({
228
+ id: plugin.id,
229
+ active: plugin.active || false,
230
+ available: true, // Could be enhanced to check endpoint availability
231
+ }));
232
+ }
233
+ // Execute a chat request through the active plugin
234
+ async executePluginRequest(model, messages, options = {}) {
235
+ // Validate model parameter to prevent SSRF attacks
236
+ if (!model || typeof model !== 'string') {
237
+ throw new Error('Invalid model parameter: must be a non-empty string');
238
+ }
239
+ // Sanitize model parameter - only allow alphanumeric, hyphens, underscores, colons, and dots
240
+ const modelPattern = /^[a-zA-Z0-9\-_:.]+$/;
241
+ if (!modelPattern.test(model)) {
242
+ throw new Error(`Invalid model parameter: ${model} contains invalid characters`);
243
+ }
244
+ // Prevent path traversal and other malicious patterns
245
+ if (model.includes('..') || model.includes('//') || model.includes('\\')) {
246
+ throw new Error(`Invalid model parameter: ${model} contains invalid patterns`);
247
+ }
248
+ const activePlugin = this.getActivePluginForModel(model);
249
+ if (!activePlugin) {
250
+ throw new Error(`No active plugin found for model: ${model}`);
251
+ }
252
+ // Additional validation: ensure the model is in the plugin's allowed model_map
253
+ if (!activePlugin.model_map.includes(model)) {
254
+ throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
255
+ }
256
+ if (!activePlugin) {
257
+ throw new Error(`No active plugin found for model: ${model}`);
258
+ }
259
+ // Get API key from environment
260
+ const apiKey = process.env[activePlugin.auth.key_env];
261
+ if (!apiKey) {
262
+ throw new Error(`API key not found in environment variable: ${activePlugin.auth.key_env}`);
263
+ }
264
+ // Prepare headers
265
+ const headers = {
266
+ 'Content-Type': 'application/json',
267
+ };
268
+ const authValue = activePlugin.auth.prefix
269
+ ? `${activePlugin.auth.prefix}${apiKey}`
270
+ : apiKey;
271
+ headers[activePlugin.auth.header] = authValue;
272
+ // Prepare request payload based on plugin type
273
+ let payload;
274
+ if (activePlugin.id === 'anthropic') {
275
+ // Anthropic-specific payload format
276
+ // Separate system messages from user/assistant messages
277
+ const systemMessages = messages.filter(msg => msg.role === 'system');
278
+ const nonSystemMessages = messages.filter(msg => msg.role !== 'system');
279
+ // Convert messages to Anthropic format with image support
280
+ const anthropicMessages = nonSystemMessages.map(msg => {
281
+ // Check if message has images
282
+ if (msg.images && msg.images.length > 0) {
283
+ // Anthropic format: content is an array of content blocks
284
+ const contentBlocks = [];
285
+ // Add images first
286
+ for (const image of msg.images) {
287
+ // Extract base64 data and media type from data URL
288
+ let base64Data = image;
289
+ let mediaType = 'image/jpeg'; // Default
290
+ if (image.startsWith('data:')) {
291
+ const match = image.match(/^data:([^;]+);base64,(.+)$/);
292
+ if (match) {
293
+ mediaType = match[1];
294
+ base64Data = match[2];
295
+ }
296
+ }
297
+ contentBlocks.push({
298
+ type: 'image',
299
+ source: {
300
+ type: 'base64',
301
+ media_type: mediaType,
302
+ data: base64Data,
303
+ },
304
+ });
305
+ }
306
+ // Add text content
307
+ if (msg.content) {
308
+ contentBlocks.push({
309
+ type: 'text',
310
+ text: msg.content,
311
+ });
312
+ }
313
+ return {
314
+ role: msg.role,
315
+ content: contentBlocks,
316
+ };
317
+ }
318
+ // No images - simple text content
319
+ return {
320
+ role: msg.role,
321
+ content: msg.content,
322
+ };
323
+ });
324
+ payload = {
325
+ model,
326
+ messages: anthropicMessages,
327
+ max_tokens: options.num_predict && options.num_predict !== -1
328
+ ? options.num_predict
329
+ : 1024,
330
+ temperature: options.temperature || 0.7,
331
+ top_p: options.top_p,
332
+ stop_sequences: options.stop,
333
+ stream: false,
334
+ };
335
+ // Add system message as top-level parameter if present
336
+ if (systemMessages.length > 0) {
337
+ payload.system = systemMessages.map(msg => msg.content).join('\n');
338
+ }
339
+ // Add required anthropic-version header
340
+ headers['anthropic-version'] = '2023-06-01';
341
+ }
342
+ else if (activePlugin.id === 'gemini') {
343
+ // Gemini-specific payload format with image support
344
+ const lastMessage = messages[messages.length - 1];
345
+ const parts = [];
346
+ // Add images if present
347
+ if (lastMessage?.images && lastMessage.images.length > 0) {
348
+ for (const image of lastMessage.images) {
349
+ let base64Data = image;
350
+ let mimeType = 'image/jpeg';
351
+ if (image.startsWith('data:')) {
352
+ const match = image.match(/^data:([^;]+);base64,(.+)$/);
353
+ if (match) {
354
+ mimeType = match[1];
355
+ base64Data = match[2];
356
+ }
357
+ }
358
+ parts.push({
359
+ inline_data: {
360
+ mime_type: mimeType,
361
+ data: base64Data,
362
+ },
363
+ });
364
+ }
365
+ }
366
+ // Add text content
367
+ if (lastMessage?.content) {
368
+ parts.push({ text: lastMessage.content });
369
+ }
370
+ payload = {
371
+ contents: [{ parts }],
372
+ generationConfig: {
373
+ temperature: options.temperature || 0.7,
374
+ maxOutputTokens: options.num_predict && options.num_predict !== -1
375
+ ? options.num_predict
376
+ : 1024,
377
+ topP: options.top_p,
378
+ stopSequences: options.stop,
379
+ },
380
+ };
381
+ }
382
+ else {
383
+ // Default OpenAI-compatible format with image support
384
+ const openaiMessages = messages.map(msg => {
385
+ // Check if message has images (OpenAI vision format)
386
+ if (msg.images && msg.images.length > 0) {
387
+ const content = [];
388
+ // Add images
389
+ for (const image of msg.images) {
390
+ // OpenAI expects data URLs or regular URLs
391
+ const imageUrl = image.startsWith('data:')
392
+ ? image
393
+ : `data:image/jpeg;base64,${image}`;
394
+ content.push({
395
+ type: 'image_url',
396
+ image_url: { url: imageUrl },
397
+ });
398
+ }
399
+ // Add text
400
+ if (msg.content) {
401
+ content.push({ type: 'text', text: msg.content });
402
+ }
403
+ return { role: msg.role, content };
404
+ }
405
+ return { role: msg.role, content: msg.content };
406
+ });
407
+ payload = {
408
+ model,
409
+ messages: openaiMessages,
410
+ temperature: options.temperature || 0.7,
411
+ max_tokens: options.num_predict === -1 ? undefined : options.num_predict,
412
+ top_p: options.top_p,
413
+ stop: options.stop,
414
+ stream: false,
415
+ };
416
+ }
417
+ // Process endpoint template - replace {model} with actual model name
418
+ // Final validation before URL construction to prevent SSRF
419
+ const sanitizedModel = encodeURIComponent(model);
420
+ const processedEndpoint = activePlugin.endpoint.replace('{model}', sanitizedModel);
421
+ // Validate the final endpoint URL
422
+ try {
423
+ const url = new URL(processedEndpoint);
424
+ // Allow HTTP only for localhost/127.0.0.1 (safe for local development)
425
+ const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
426
+ if (url.protocol !== 'https:' && !isLocalhost) {
427
+ throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints. ` +
428
+ `(HTTP is permitted only for localhost during local development)`);
429
+ }
430
+ }
431
+ catch (_error) {
432
+ throw new Error(`Invalid endpoint URL constructed: ${processedEndpoint}`);
433
+ }
434
+ try {
435
+ const response = await axios.post(processedEndpoint, payload, {
436
+ headers,
437
+ timeout: 60000, // 60 second timeout
438
+ });
439
+ // Handle different response formats
440
+ if (activePlugin.id === 'anthropic') {
441
+ return this.convertAnthropicResponse(response.data, model);
442
+ }
443
+ else if (activePlugin.id === 'gemini') {
444
+ return this.convertGeminiResponse(response.data, model);
445
+ }
446
+ // Default to OpenAI format
447
+ return response.data;
448
+ }
449
+ catch (error) {
450
+ console.error(`Plugin request failed for ${activePlugin.id}:`, error);
451
+ if (error && typeof error === 'object' && 'response' in error) {
452
+ const axiosError = error;
453
+ throw new Error(`Plugin API error: ${axiosError.response.status} - ${axiosError.response.data?.error?.message || axiosError.response.statusText}`);
454
+ }
455
+ else if (error && typeof error === 'object' && 'request' in error) {
456
+ throw new Error(`Plugin connection error: Unable to reach ${processedEndpoint}`);
457
+ }
458
+ else {
459
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
460
+ throw new Error(`Plugin error: ${errorMessage}`);
461
+ }
462
+ }
463
+ }
464
+ // Convert Anthropic response format to OpenAI format
465
+ convertAnthropicResponse(anthropicResponse, model) {
466
+ const id = typeof anthropicResponse.id === 'string'
467
+ ? anthropicResponse.id
468
+ : `chatcmpl-${Date.now()}`;
469
+ // Map Anthropic stop reasons to OpenAI format
470
+ const stopReasonMap = {
471
+ end_turn: 'stop',
472
+ max_tokens: 'length',
473
+ stop_sequence: 'stop',
474
+ tool_use: 'tool_calls',
475
+ };
476
+ const stopReason = typeof anthropicResponse.stop_reason === 'string'
477
+ ? stopReasonMap[anthropicResponse.stop_reason] || 'stop'
478
+ : 'stop';
479
+ let content = '';
480
+ // Anthropic returns content as an array of content blocks
481
+ if (Array.isArray(anthropicResponse.content)) {
482
+ for (const block of anthropicResponse.content) {
483
+ if (block &&
484
+ typeof block === 'object' &&
485
+ 'type' in block &&
486
+ block.type === 'text' &&
487
+ 'text' in block &&
488
+ typeof block.text === 'string') {
489
+ content += block.text;
490
+ }
491
+ }
492
+ }
493
+ let usage;
494
+ if (anthropicResponse.usage &&
495
+ typeof anthropicResponse.usage === 'object' &&
496
+ anthropicResponse.usage !== null) {
497
+ const usageObj = anthropicResponse.usage;
498
+ const inputTokens = typeof usageObj.input_tokens === 'number' ? usageObj.input_tokens : 0;
499
+ const outputTokens = typeof usageObj.output_tokens === 'number' ? usageObj.output_tokens : 0;
500
+ usage = {
501
+ prompt_tokens: inputTokens,
502
+ completion_tokens: outputTokens,
503
+ total_tokens: inputTokens + outputTokens,
504
+ };
505
+ }
506
+ return {
507
+ id,
508
+ object: 'chat.completion',
509
+ created: Math.floor(Date.now() / 1000),
510
+ model,
511
+ choices: [
512
+ {
513
+ index: 0,
514
+ message: {
515
+ role: 'assistant',
516
+ content,
517
+ },
518
+ finish_reason: stopReason,
519
+ },
520
+ ],
521
+ usage,
522
+ };
523
+ }
524
+ // Convert Gemini response format to OpenAI format
525
+ convertGeminiResponse(geminiResponse, model) {
526
+ const id = `chatcmpl-${Date.now()}`;
527
+ let content = '';
528
+ let finishReason = 'stop';
529
+ // Gemini returns candidates array
530
+ if (Array.isArray(geminiResponse.candidates)) {
531
+ const candidate = geminiResponse.candidates[0];
532
+ if (candidate && typeof candidate === 'object') {
533
+ const candidateObj = candidate;
534
+ // Extract content from parts
535
+ if (candidateObj.content && typeof candidateObj.content === 'object') {
536
+ const contentObj = candidateObj.content;
537
+ if (Array.isArray(contentObj.parts)) {
538
+ for (const part of contentObj.parts) {
539
+ if (part &&
540
+ typeof part === 'object' &&
541
+ 'text' in part &&
542
+ typeof part.text === 'string') {
543
+ content += part.text;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ // Map Gemini finish reason to OpenAI format
549
+ if (typeof candidateObj.finishReason === 'string') {
550
+ const finishReasonMap = {
551
+ STOP: 'stop',
552
+ MAX_TOKENS: 'length',
553
+ SAFETY: 'content_filter',
554
+ RECITATION: 'content_filter',
555
+ OTHER: 'stop',
556
+ };
557
+ finishReason = finishReasonMap[candidateObj.finishReason] || 'stop';
558
+ }
559
+ }
560
+ }
561
+ // Extract usage if available
562
+ let usage;
563
+ if (geminiResponse.usageMetadata &&
564
+ typeof geminiResponse.usageMetadata === 'object') {
565
+ const usageObj = geminiResponse.usageMetadata;
566
+ const promptTokens = typeof usageObj.promptTokenCount === 'number'
567
+ ? usageObj.promptTokenCount
568
+ : 0;
569
+ const completionTokens = typeof usageObj.candidatesTokenCount === 'number'
570
+ ? usageObj.candidatesTokenCount
571
+ : 0;
572
+ usage = {
573
+ prompt_tokens: promptTokens,
574
+ completion_tokens: completionTokens,
575
+ total_tokens: promptTokens + completionTokens,
576
+ };
577
+ }
578
+ return {
579
+ id,
580
+ object: 'chat.completion',
581
+ created: Math.floor(Date.now() / 1000),
582
+ model,
583
+ choices: [
584
+ {
585
+ index: 0,
586
+ message: {
587
+ role: 'assistant',
588
+ content,
589
+ },
590
+ finish_reason: finishReason,
591
+ },
592
+ ],
593
+ usage,
594
+ };
595
+ }
596
+ // Validate plugin structure
597
+ validatePlugin(plugin) {
598
+ return (typeof plugin === 'object' &&
599
+ plugin !== null &&
600
+ typeof plugin.id === 'string' &&
601
+ typeof plugin.name === 'string' &&
602
+ typeof plugin.type === 'string' &&
603
+ typeof plugin.endpoint === 'string' &&
604
+ typeof plugin.auth === 'object' &&
605
+ plugin.auth !== null &&
606
+ typeof plugin.auth.header === 'string' &&
607
+ typeof plugin.auth.key_env === 'string' &&
608
+ (plugin.auth
609
+ .prefix === undefined ||
610
+ typeof plugin.auth.prefix === 'string') &&
611
+ Array.isArray(plugin.model_map) &&
612
+ plugin.model_map.length > 0);
613
+ }
614
+ // Export plugin to JSON
615
+ exportPlugin(id) {
616
+ return this.getPlugin(id);
617
+ }
618
+ // Import plugin from JSON data
619
+ importPlugin(pluginData) {
620
+ // Validate and clean the plugin data
621
+ if (!this.validatePlugin(pluginData)) {
622
+ throw new Error('Invalid plugin data');
623
+ }
624
+ // Check if plugin already exists
625
+ const existingPlugin = this.getPlugin(pluginData.id);
626
+ if (existingPlugin) {
627
+ throw new Error(`Plugin with ID ${pluginData.id} already exists`);
628
+ }
629
+ return this.installPlugin(pluginData);
630
+ }
631
+ // ============================================
632
+ // TTS (Text-to-Speech) Methods
633
+ // ============================================
634
+ // Get plugin that supports TTS for a specific model
635
+ getPluginForTTS(model) {
636
+ console.log(`[DEBUG] Looking for TTS plugin for model: ${model}`);
637
+ const allPlugins = this.getAllPlugins();
638
+ for (const plugin of allPlugins) {
639
+ // Check if plugin has TTS capability
640
+ if (plugin.capabilities?.tts) {
641
+ const ttsCapability = plugin.capabilities.tts;
642
+ if (ttsCapability.model_map.includes(model)) {
643
+ console.log(`[DEBUG] Found TTS plugin ${plugin.id} for model ${model}`);
644
+ // Check if we have the required API key
645
+ const apiKey = process.env[plugin.auth.key_env];
646
+ if (!apiKey) {
647
+ console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env} not set`);
648
+ continue;
649
+ }
650
+ return plugin;
651
+ }
652
+ }
653
+ // Also check primary type for backward compatibility with TTS-only plugins
654
+ if (plugin.type === 'tts' && plugin.model_map.includes(model)) {
655
+ console.log(`[DEBUG] Found TTS-type plugin ${plugin.id} for model ${model}`);
656
+ const apiKey = process.env[plugin.auth.key_env];
657
+ if (!apiKey) {
658
+ console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env} not set`);
659
+ continue;
660
+ }
661
+ return plugin;
662
+ }
663
+ }
664
+ console.log(`[DEBUG] No TTS plugin found for model: ${model}`);
665
+ return null;
666
+ }
667
+ // Get all available TTS models from all plugins
668
+ getAvailableTTSModels() {
669
+ const models = [];
670
+ const allPlugins = this.getAllPlugins();
671
+ for (const plugin of allPlugins) {
672
+ // Check capabilities-based TTS
673
+ if (plugin.capabilities?.tts) {
674
+ const ttsCapability = plugin.capabilities.tts;
675
+ // Check if API key is available
676
+ const apiKey = process.env[plugin.auth.key_env];
677
+ if (apiKey) {
678
+ for (const model of ttsCapability.model_map) {
679
+ models.push({
680
+ model,
681
+ plugin: plugin.id,
682
+ config: ttsCapability.config,
683
+ });
684
+ }
685
+ }
686
+ }
687
+ // Check primary type for TTS-only plugins
688
+ if (plugin.type === 'tts') {
689
+ const apiKey = process.env[plugin.auth.key_env];
690
+ if (apiKey) {
691
+ for (const model of plugin.model_map) {
692
+ models.push({
693
+ model,
694
+ plugin: plugin.id,
695
+ });
696
+ }
697
+ }
698
+ }
699
+ }
700
+ return models;
701
+ }
702
+ // Execute a TTS request through the appropriate plugin
703
+ async executeTTSRequest(model, input, options = {}) {
704
+ // Validate model parameter
705
+ if (!model || typeof model !== 'string') {
706
+ throw new Error('Invalid model parameter: must be a non-empty string');
707
+ }
708
+ // Sanitize model parameter
709
+ const modelPattern = /^[a-zA-Z0-9\-_:.]+$/;
710
+ if (!modelPattern.test(model)) {
711
+ throw new Error(`Invalid model parameter: ${model} contains invalid characters`);
712
+ }
713
+ // Prevent path traversal
714
+ if (model.includes('..') || model.includes('//') || model.includes('\\')) {
715
+ throw new Error(`Invalid model parameter: ${model} contains invalid patterns`);
716
+ }
717
+ const plugin = this.getPluginForTTS(model);
718
+ if (!plugin) {
719
+ throw new Error(`No TTS plugin found for model: ${model}`);
720
+ }
721
+ // Get API key from environment
722
+ const apiKey = process.env[plugin.auth.key_env];
723
+ if (!apiKey) {
724
+ throw new Error(`API key not found in environment variable: ${plugin.auth.key_env}`);
725
+ }
726
+ // Determine endpoint
727
+ let endpoint;
728
+ let ttsConfig;
729
+ if (plugin.capabilities?.tts) {
730
+ endpoint = plugin.capabilities.tts.endpoint;
731
+ ttsConfig = plugin.capabilities.tts.config;
732
+ }
733
+ else {
734
+ endpoint = plugin.endpoint;
735
+ }
736
+ // Prepare headers
737
+ const headers = {
738
+ 'Content-Type': 'application/json',
739
+ };
740
+ const authValue = plugin.auth.prefix
741
+ ? `${plugin.auth.prefix}${apiKey}`
742
+ : apiKey;
743
+ headers[plugin.auth.header] = authValue;
744
+ // Apply defaults from config
745
+ const voice = options.voice || ttsConfig?.default_voice || 'alloy';
746
+ const responseFormat = options.response_format || ttsConfig?.default_format || 'mp3';
747
+ const speed = options.speed || 1.0;
748
+ // Check if input needs chunking (for long texts)
749
+ const maxChars = ttsConfig?.max_characters || 4096;
750
+ if (input.length > maxChars) {
751
+ // Split text into chunks and process each, then concatenate audio
752
+ const chunks = this.splitTextForTTS(input, maxChars);
753
+ console.log(`[TTS] Input too long (${input.length} chars), splitting into ${chunks.length} chunks`);
754
+ const audioBuffers = [];
755
+ for (let i = 0; i < chunks.length; i++) {
756
+ console.log(`[TTS] Processing chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
757
+ // Recursive call with chunk (will not re-chunk since it's under limit)
758
+ const chunkAudio = await this.executeTTSRequest(model, chunks[i], options);
759
+ audioBuffers.push(chunkAudio);
760
+ }
761
+ // Concatenate all audio buffers
762
+ return Buffer.concat(audioBuffers);
763
+ }
764
+ // Prepare request payload and endpoint based on plugin type
765
+ let payload;
766
+ let processedEndpoint;
767
+ if (plugin.id === 'elevenlabs') {
768
+ // ElevenLabs API format
769
+ // ElevenLabs uses voice IDs - map voice names to IDs
770
+ const elevenLabsVoiceIds = {
771
+ rachel: '21m00Tcm4TlvDq8ikWAM',
772
+ domi: 'AZnzlk1XvdvUeBnXmlld',
773
+ bella: 'EXAVITQu4vr4xnSDxMaL',
774
+ antoni: 'ErXwobaYiN019PkySvjV',
775
+ elli: 'MF3mGyEYCl7XYWbV9V6O',
776
+ josh: 'TxGEqnHWrfWFTfGW9XjX',
777
+ arnold: 'VR6AewLTigWG4xSOukaG',
778
+ adam: 'pNInz6obpgDQGcFmaJgB',
779
+ sam: 'yoZ06aMxZJJ28mfd3POQ',
780
+ nicole: 'piTKgcLEGmPE4e6mEKli',
781
+ glinda: 'z9fAnlkpzviPz146aGWa',
782
+ clyde: '2EiwWnXFnvU5JabPnv8n',
783
+ james: 'ZQe5CZNOzWyzPSCn5a3c',
784
+ charlotte: 'XB0fDUnXU5powFXDhCwa',
785
+ lily: 'pFZP5JQG7iQjIQuC4Bku',
786
+ serena: 'pMsXgVXv3BLzUgSXRplE',
787
+ };
788
+ const voiceId = elevenLabsVoiceIds[voice.toLowerCase()] ||
789
+ elevenLabsVoiceIds['rachel'] ||
790
+ '21m00Tcm4TlvDq8ikWAM';
791
+ processedEndpoint = `${endpoint}/${voiceId}`;
792
+ // Add output_format query parameter
793
+ const formatMap = {
794
+ mp3: 'mp3_44100_128',
795
+ pcm: 'pcm_16000',
796
+ ulaw: 'ulaw_8000',
797
+ };
798
+ const outputFormat = formatMap[responseFormat] || 'mp3_44100_128';
799
+ processedEndpoint += `?output_format=${outputFormat}`;
800
+ payload = {
801
+ text: input,
802
+ model_id: model,
803
+ voice_settings: {
804
+ stability: 0.5,
805
+ similarity_boost: 0.75,
806
+ },
807
+ };
808
+ }
809
+ else {
810
+ // Default OpenAI TTS format
811
+ payload = {
812
+ model,
813
+ input,
814
+ voice,
815
+ response_format: responseFormat,
816
+ speed,
817
+ };
818
+ // Process endpoint template
819
+ const sanitizedModel = encodeURIComponent(model);
820
+ processedEndpoint = endpoint.replace('{model}', sanitizedModel);
821
+ }
822
+ // Validate the final endpoint URL
823
+ try {
824
+ const url = new URL(processedEndpoint);
825
+ const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
826
+ if (url.protocol !== 'https:' && !isLocalhost) {
827
+ throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints.`);
828
+ }
829
+ }
830
+ catch (_error) {
831
+ throw new Error(`Invalid endpoint URL constructed: ${processedEndpoint}`);
832
+ }
833
+ try {
834
+ const response = await axios.post(processedEndpoint, payload, {
835
+ headers,
836
+ timeout: 120000, // 2 minute timeout for TTS
837
+ responseType: 'arraybuffer', // TTS returns binary audio data
838
+ });
839
+ return Buffer.from(response.data);
840
+ }
841
+ catch (error) {
842
+ console.error(`TTS plugin request failed for ${plugin.id}:`, error);
843
+ if (error && typeof error === 'object' && 'response' in error) {
844
+ const axiosError = error;
845
+ // Try to parse error message from response
846
+ let errorMessage = axiosError.response.statusText;
847
+ if (axiosError.response.data) {
848
+ try {
849
+ const errorText = Buffer.from(axiosError.response.data).toString('utf8');
850
+ const errorJson = JSON.parse(errorText);
851
+ errorMessage = errorJson.error?.message || errorMessage;
852
+ }
853
+ catch {
854
+ // Ignore parse errors
855
+ }
856
+ }
857
+ throw new Error(`TTS API error: ${axiosError.response.status} - ${errorMessage}`);
858
+ }
859
+ else if (error && typeof error === 'object' && 'request' in error) {
860
+ throw new Error(`TTS connection error: Unable to reach ${processedEndpoint}`);
861
+ }
862
+ else {
863
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
864
+ throw new Error(`TTS error: ${errorMessage}`);
865
+ }
866
+ }
867
+ }
868
+ // Split text into chunks for TTS, trying to break at sentence boundaries
869
+ splitTextForTTS(text, maxChars) {
870
+ const chunks = [];
871
+ let remaining = text;
872
+ while (remaining.length > 0) {
873
+ if (remaining.length <= maxChars) {
874
+ chunks.push(remaining);
875
+ break;
876
+ }
877
+ // Try to find a good break point (sentence end) within the limit
878
+ let breakPoint = maxChars;
879
+ const searchStart = Math.max(0, maxChars - 500); // Look in last 500 chars for sentence end
880
+ // Look for sentence endings (. ! ?) followed by space or end
881
+ const sentenceEnders = ['. ', '! ', '? ', '.\n', '!\n', '?\n'];
882
+ let bestBreak = -1;
883
+ for (const ender of sentenceEnders) {
884
+ const lastIndex = remaining.lastIndexOf(ender, maxChars);
885
+ if (lastIndex > searchStart && lastIndex > bestBreak) {
886
+ bestBreak = lastIndex + ender.length;
887
+ }
888
+ }
889
+ if (bestBreak > searchStart) {
890
+ breakPoint = bestBreak;
891
+ }
892
+ else {
893
+ // Fall back to breaking at whitespace
894
+ const lastSpace = remaining.lastIndexOf(' ', maxChars);
895
+ if (lastSpace > searchStart) {
896
+ breakPoint = lastSpace + 1;
897
+ }
898
+ // If no good break found, just break at maxChars (may split mid-word)
899
+ }
900
+ chunks.push(remaining.slice(0, breakPoint).trim());
901
+ remaining = remaining.slice(breakPoint).trim();
902
+ }
903
+ return chunks.filter(chunk => chunk.length > 0);
904
+ }
905
+ // Get TTS configuration for a specific plugin
906
+ getTTSConfig(pluginId) {
907
+ const plugin = this.getPlugin(pluginId);
908
+ if (!plugin)
909
+ return null;
910
+ if (plugin.capabilities?.tts?.config) {
911
+ return plugin.capabilities.tts.config;
912
+ }
913
+ return null;
914
+ }
915
+ // Get all plugins that support a specific capability type
916
+ getPluginsByCapability(capabilityType) {
917
+ const allPlugins = this.getAllPlugins();
918
+ const result = [];
919
+ for (const plugin of allPlugins) {
920
+ // Check if primary type matches
921
+ if (plugin.type === capabilityType) {
922
+ const apiKey = process.env[plugin.auth.key_env];
923
+ if (apiKey) {
924
+ result.push(plugin);
925
+ }
926
+ continue;
927
+ }
928
+ // Check capabilities object based on capability type
929
+ if (plugin.capabilities) {
930
+ let hasCapability = false;
931
+ switch (capabilityType) {
932
+ case 'tts':
933
+ hasCapability = !!plugin.capabilities.tts;
934
+ break;
935
+ case 'stt':
936
+ hasCapability = !!plugin.capabilities.stt;
937
+ break;
938
+ case 'embedding':
939
+ hasCapability = !!plugin.capabilities.embedding;
940
+ break;
941
+ case 'image':
942
+ hasCapability = !!plugin.capabilities.image;
943
+ break;
944
+ case 'completion':
945
+ case 'chat':
946
+ hasCapability = !!plugin.capabilities.completion;
947
+ break;
948
+ }
949
+ if (hasCapability) {
950
+ const apiKey = process.env[plugin.auth.key_env];
951
+ if (apiKey) {
952
+ result.push(plugin);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ return result;
958
+ }
959
+ }
960
+ export default new PluginService();
961
+ //# sourceMappingURL=pluginService.js.map