@cloudcli-ai/cloudcli 1.34.0 → 1.35.1

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 (247) hide show
  1. package/README.de.md +9 -1
  2. package/README.ja.md +8 -0
  3. package/README.ko.md +9 -1
  4. package/README.md +23 -5
  5. package/README.ru.md +9 -1
  6. package/README.tr.md +8 -1
  7. package/README.zh-CN.md +9 -1
  8. package/README.zh-TW.md +9 -1
  9. package/dist/api-docs.html +36 -18
  10. package/dist/assets/index-BvX30NW5.css +32 -0
  11. package/dist/assets/index-D1IdCTT6.js +1418 -0
  12. package/dist/index.html +12 -4
  13. package/dist-server/server/browser-use-mcp.js +350 -0
  14. package/dist-server/server/browser-use-mcp.js.map +1 -0
  15. package/dist-server/server/claude-sdk.js +25 -5
  16. package/dist-server/server/claude-sdk.js.map +1 -1
  17. package/dist-server/server/cli.js +14 -6
  18. package/dist-server/server/cli.js.map +1 -1
  19. package/dist-server/server/cursor-cli.js +27 -11
  20. package/dist-server/server/cursor-cli.js.map +1 -1
  21. package/dist-server/server/gemini-cli.js +17 -2
  22. package/dist-server/server/gemini-cli.js.map +1 -1
  23. package/dist-server/server/index.js +136 -40
  24. package/dist-server/server/index.js.map +1 -1
  25. package/dist-server/server/load-env.js +1 -1
  26. package/dist-server/server/load-env.js.map +1 -1
  27. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  28. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  29. package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
  30. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  31. package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
  32. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  33. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  34. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  35. package/dist-server/server/modules/database/index.js +1 -0
  36. package/dist-server/server/modules/database/index.js.map +1 -1
  37. package/dist-server/server/modules/database/migrations.js +23 -1
  38. package/dist-server/server/modules/database/migrations.js.map +1 -1
  39. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  40. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  41. package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
  42. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
  43. package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
  44. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  45. package/dist-server/server/modules/database/schema.js +26 -0
  46. package/dist-server/server/modules/database/schema.js.map +1 -1
  47. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  48. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
  49. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  50. package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
  51. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  52. package/dist-server/server/modules/notifications/index.js +4 -0
  53. package/dist-server/server/modules/notifications/index.js.map +1 -0
  54. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  55. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  56. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  57. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  58. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
  59. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  60. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  61. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  62. package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
  63. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  64. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
  65. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  66. package/dist-server/server/modules/providers/index.js +1 -0
  67. package/dist-server/server/modules/providers/index.js.map +1 -1
  68. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
  70. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  72. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
  73. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
  74. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
  79. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  80. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
  81. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  82. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
  84. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
  85. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
  86. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  88. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
  89. package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
  90. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
  91. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  92. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
  93. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
  94. package/dist-server/server/modules/providers/provider.routes.js +123 -1
  95. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  96. package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
  97. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  98. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
  99. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  100. package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
  101. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  102. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
  103. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  104. package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
  105. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  106. package/dist-server/server/modules/providers/services/skills.service.js +11 -0
  107. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
  108. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
  109. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
  110. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
  111. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  112. package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
  113. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  114. package/dist-server/server/modules/websocket/index.js +1 -0
  115. package/dist-server/server/modules/websocket/index.js.map +1 -1
  116. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
  117. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  118. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
  119. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  120. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
  121. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  122. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
  123. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  124. package/dist-server/server/modules/websocket/services/websocket-server.service.js +5 -0
  125. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  126. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
  127. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  128. package/dist-server/server/openai-codex.js +24 -14
  129. package/dist-server/server/openai-codex.js.map +1 -1
  130. package/dist-server/server/opencode-cli.js +21 -8
  131. package/dist-server/server/opencode-cli.js.map +1 -1
  132. package/dist-server/server/opencode-cli.test.js +19 -1
  133. package/dist-server/server/opencode-cli.test.js.map +1 -1
  134. package/dist-server/server/routes/settings.js +1 -1
  135. package/dist-server/server/routes/settings.js.map +1 -1
  136. package/dist-server/server/services/notification-orchestrator.js +1 -176
  137. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  138. package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
  139. package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
  140. package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
  141. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  142. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  143. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  144. package/dist-server/server/shared/utils.js +73 -1
  145. package/dist-server/server/shared/utils.js.map +1 -1
  146. package/dist-server/server/voice-proxy.js +226 -0
  147. package/dist-server/server/voice-proxy.js.map +1 -0
  148. package/electron/assets/logo-macos.icns +0 -0
  149. package/electron/assets/logo-macos.png +0 -0
  150. package/electron/assets/logo-windows.ico +0 -0
  151. package/electron/cloud.js +260 -0
  152. package/electron/desktopNotifications.js +378 -0
  153. package/electron/desktopWindow.js +766 -0
  154. package/electron/launcher/index.html +14 -0
  155. package/electron/launcher/launcher.css +801 -0
  156. package/electron/launcher/launcher.js +687 -0
  157. package/electron/localServer.js +550 -0
  158. package/electron/main.js +944 -0
  159. package/electron/preload.cjs +60 -0
  160. package/electron/scripts/generate-macos-icon.js +62 -0
  161. package/electron/serverInstaller.js +277 -0
  162. package/electron/tabs.js +87 -0
  163. package/electron/viewHost.js +331 -0
  164. package/package.json +79 -3
  165. package/public/api-docs.html +36 -18
  166. package/scripts/release/build-server-bundle.js +176 -0
  167. package/scripts/release/prepare-desktop-app.js +152 -0
  168. package/server/browser-use-mcp.ts +384 -0
  169. package/server/claude-sdk.js +27 -5
  170. package/server/cli.js +15 -6
  171. package/server/cursor-cli.js +27 -11
  172. package/server/gemini-cli.js +17 -2
  173. package/server/index.js +137 -47
  174. package/server/load-env.js +1 -1
  175. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  176. package/server/modules/browser-use/browser-use.routes.ts +96 -0
  177. package/server/modules/browser-use/browser-use.service.ts +836 -0
  178. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  179. package/server/modules/database/index.ts +1 -0
  180. package/server/modules/database/migrations.ts +25 -0
  181. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  182. package/server/modules/database/repositories/notification-preferences.ts +12 -1
  183. package/server/modules/database/repositories/sessions.db.ts +227 -23
  184. package/server/modules/database/schema.ts +27 -0
  185. package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
  186. package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
  187. package/server/modules/notifications/index.ts +13 -0
  188. package/server/modules/notifications/notifications.routes.ts +127 -0
  189. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  190. package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
  191. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  192. package/server/modules/projects/services/project-management.service.ts +0 -8
  193. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
  194. package/server/modules/providers/README.md +1 -1
  195. package/server/modules/providers/index.ts +1 -0
  196. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
  197. package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
  198. package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
  199. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
  200. package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
  201. package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
  202. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
  203. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
  204. package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
  205. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
  206. package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
  207. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
  208. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
  209. package/server/modules/providers/provider.routes.ts +167 -1
  210. package/server/modules/providers/services/mcp.service.ts +26 -0
  211. package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
  212. package/server/modules/providers/services/provider-models.service.ts +1 -1
  213. package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
  214. package/server/modules/providers/services/sessions.service.ts +79 -4
  215. package/server/modules/providers/services/skills.service.ts +25 -1
  216. package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
  217. package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
  218. package/server/modules/providers/tests/skills.test.ts +212 -0
  219. package/server/modules/websocket/README.md +33 -27
  220. package/server/modules/websocket/index.ts +1 -0
  221. package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
  222. package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
  223. package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
  224. package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
  225. package/server/modules/websocket/services/websocket-server.service.ts +6 -0
  226. package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
  227. package/server/openai-codex.js +24 -14
  228. package/server/opencode-cli.js +21 -8
  229. package/server/opencode-cli.test.js +20 -1
  230. package/server/routes/settings.js +6 -1
  231. package/server/services/notification-orchestrator.js +3 -224
  232. package/server/services/tests/notification-orchestrator.test.js +80 -0
  233. package/server/shared/interfaces.ts +15 -0
  234. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  235. package/server/shared/types.ts +84 -1
  236. package/server/shared/utils.ts +95 -1
  237. package/server/voice-proxy.js +224 -0
  238. package/dist/assets/index-73Xw3i9v.css +0 -32
  239. package/dist/assets/index-BIHaviaA.js +0 -1361
  240. package/dist/modelConstants.js +0 -854
  241. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
  242. package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
  243. package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
  244. package/public/modelConstants.js +0 -854
  245. /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
  246. /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
  247. /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
@@ -0,0 +1,224 @@
1
+ // Optional voice proxy — forwards STT/TTS to an OpenAI-compatible audio backend.
2
+ //
3
+ // The backend is whatever the user points at: OpenAI, Groq, or a local server
4
+ // (LocalAI / Speaches / Kokoro-FastAPI / openedai-speech / etc.). It must expose the
5
+ // standard OpenAI audio endpoints:
6
+ // POST {base}/audio/transcriptions (multipart 'file' + 'model') -> { text }
7
+ // POST {base}/audio/speech ({ model, voice, input }) -> audio bytes
8
+ //
9
+ // Config is resolved per-request from headers (set by the client's voice settings),
10
+ // falling back to server env defaults. Mounted at /api/voice behind authenticateToken.
11
+ import { Readable } from 'node:stream';
12
+
13
+ import express from 'express';
14
+
15
+ const ENV = {
16
+ baseUrl: (process.env.VOICE_API_BASE_URL || '').replace(/\/$/, ''),
17
+ apiKey: process.env.VOICE_API_KEY || '',
18
+ sttModel: process.env.VOICE_STT_MODEL || 'whisper-1',
19
+ ttsModel: process.env.VOICE_TTS_MODEL || 'tts-1',
20
+ ttsVoice: process.env.VOICE_TTS_VOICE || 'alloy',
21
+ };
22
+
23
+ /**
24
+ * Resolve the voice backend config for a request. Client headers (set from the
25
+ * user's in-app voice settings) take precedence over the server env defaults.
26
+ * @param {import('express').Request} req
27
+ * @returns {{baseUrl: string, apiKey: string, sttModel: string, ttsModel: string, ttsVoice: string, ttsFormat: string}}
28
+ */
29
+ function resolveConfig(req) {
30
+ const h = req.headers;
31
+ return {
32
+ // Security: do not allow clients to control the outbound backend host.
33
+ // Always use the server-side configured base URL.
34
+ baseUrl: ENV.baseUrl,
35
+ apiKey: String(h['x-voice-api-key'] || '') || ENV.apiKey,
36
+ sttModel: String(h['x-voice-stt-model'] || '') || ENV.sttModel,
37
+ ttsModel: String(h['x-voice-tts-model'] || '') || ENV.ttsModel,
38
+ ttsVoice: String(h['x-voice-tts-voice'] || '') || ENV.ttsVoice,
39
+ ttsFormat: String(h['x-voice-tts-format'] || '').trim(),
40
+ };
41
+ }
42
+
43
+ const router = express.Router();
44
+
45
+ // Generous by default — local TTS can synthesize long messages at ~real-time on CPU.
46
+ // Guard against a non-numeric/zero override that would make setTimeout fire immediately.
47
+ const DEFAULT_VOICE_TIMEOUT_MS = 300000;
48
+ const _parsedTimeout = Number(process.env.VOICE_TIMEOUT_MS);
49
+ const VOICE_TIMEOUT_MS = Number.isFinite(_parsedTimeout) && _parsedTimeout > 0
50
+ ? _parsedTimeout
51
+ : DEFAULT_VOICE_TIMEOUT_MS;
52
+
53
+ /**
54
+ * fetch() with an AbortController timeout so a stalled backend can't hold the
55
+ * request open indefinitely. Aborts after VOICE_TIMEOUT_MS.
56
+ * @param {string} url
57
+ * @param {RequestInit} [options]
58
+ * @returns {Promise<Response>}
59
+ */
60
+ async function fetchWithTimeout(url, options = {}) {
61
+ const parsed = new URL(url);
62
+ if (!['http:', 'https:'].includes(parsed.protocol) || !isAllowedBackendUrl(parsed.origin)) {
63
+ throw new Error('Blocked outbound voice backend URL');
64
+ }
65
+ const controller = new AbortController();
66
+ const timer = setTimeout(() => controller.abort(), VOICE_TIMEOUT_MS);
67
+ try {
68
+ return await fetch(parsed.toString(), { redirect: 'manual', ...options, signal: controller.signal });
69
+ } finally {
70
+ clearTimeout(timer);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Turn a backend fetch failure into a clear, actionable client response:
76
+ * 504 on timeout (AbortError), 502 otherwise.
77
+ * @param {import('express').Response} res
78
+ * @param {Error} e
79
+ */
80
+ function backendError(res, e) {
81
+ if (e && e.name === 'AbortError') {
82
+ return res.status(504).json({
83
+ error: `Voice backend timed out after ${Math.round(VOICE_TIMEOUT_MS / 1000)}s. Check your voice backend.`,
84
+ });
85
+ }
86
+ return res.status(502).json({ error: `Voice backend unreachable: ${e.message}` });
87
+ }
88
+
89
+ /**
90
+ * SSRF guard for the user-configurable backend URL: allow http/https only and
91
+ * block the link-local / cloud-metadata range (169.254.x). localhost and private
92
+ * ranges are allowed on purpose so users can point at a local voice server
93
+ * (LocalAI, Speaches, Kokoro-FastAPI, etc.).
94
+ * @param {string} raw
95
+ * @returns {boolean}
96
+ */
97
+ function isAllowedBackendUrl(raw) {
98
+ let u;
99
+ try {
100
+ u = new URL(raw);
101
+ } catch {
102
+ return false;
103
+ }
104
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
105
+ if (u.hostname === '169.254.169.254' || u.hostname.startsWith('169.254.')) return false;
106
+ return true;
107
+ }
108
+
109
+ /**
110
+ * Relay an upstream (backend) error to the client without making an upstream
111
+ * 401/403 look like the user's own app login failed.
112
+ * @param {import('express').Response} res
113
+ * @param {number} status
114
+ * @param {string} [text]
115
+ */
116
+ function upstreamError(res, status, text) {
117
+ if (status === 401 || status === 403) {
118
+ return res.status(502).json({ error: 'Voice backend rejected the request (check the API key).' });
119
+ }
120
+ return res.status(status).json({ error: text || 'voice backend error' });
121
+ }
122
+
123
+ let _upload = null;
124
+ /**
125
+ * Lazily build a memory-storage multer instance (25 MB cap) for audio uploads,
126
+ * so multer is only imported when the voice feature is actually used.
127
+ * @returns {Promise<import('multer').Multer>}
128
+ */
129
+ async function getUpload() {
130
+ if (!_upload) {
131
+ const multer = (await import('multer')).default;
132
+ _upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });
133
+ }
134
+ return _upload;
135
+ }
136
+
137
+ /**
138
+ * Build the Authorization header for the backend, or an empty object when no
139
+ * key is configured (e.g. a local server that needs none).
140
+ * @param {string} apiKey
141
+ * @returns {Record<string, string>}
142
+ */
143
+ function authHeader(apiKey) {
144
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
145
+ }
146
+
147
+ /**
148
+ * GET /api/voice/health -> { configured } (true when a backend base URL is set).
149
+ */
150
+ router.get('/health', (req, res) => {
151
+ res.json({ configured: Boolean(resolveConfig(req).baseUrl) });
152
+ });
153
+
154
+ /**
155
+ * POST /api/voice/transcribe (multipart 'audio') -> { text }.
156
+ * Forwards the uploaded audio to the backend's /audio/transcriptions endpoint.
157
+ */
158
+ router.post('/transcribe', async (req, res) => {
159
+ const cfg = resolveConfig(req);
160
+ if (!cfg.baseUrl) return res.status(503).json({ error: 'No voice backend configured' });
161
+ if (!isAllowedBackendUrl(cfg.baseUrl)) return res.status(400).json({ error: 'Invalid voice backend URL.' });
162
+ const upload = await getUpload();
163
+ upload.single('audio')(req, res, async (err) => {
164
+ if (err) return res.status(400).json({ error: err.message });
165
+ if (!req.file) return res.status(400).json({ error: 'No audio uploaded' });
166
+ try {
167
+ const fd = new FormData();
168
+ fd.append(
169
+ 'file',
170
+ new Blob([req.file.buffer], { type: req.file.mimetype || 'audio/webm' }),
171
+ req.file.originalname || 'recording.webm',
172
+ );
173
+ fd.append('model', cfg.sttModel);
174
+ const r = await fetchWithTimeout(`${cfg.baseUrl}/audio/transcriptions`, {
175
+ method: 'POST',
176
+ headers: authHeader(cfg.apiKey),
177
+ body: fd,
178
+ });
179
+ const text = await r.text();
180
+ if (!r.ok) return upstreamError(res, r.status, text);
181
+ let data;
182
+ try { data = JSON.parse(text); } catch { data = { text }; }
183
+ res.json({ text: data.text ?? '' });
184
+ } catch (e) {
185
+ backendError(res, e);
186
+ }
187
+ });
188
+ });
189
+
190
+ /**
191
+ * POST /api/voice/tts { text } -> audio bytes.
192
+ * Forwards the text to the backend's /audio/speech endpoint and streams the audio back.
193
+ */
194
+ router.post('/tts', async (req, res) => {
195
+ const cfg = resolveConfig(req);
196
+ if (!cfg.baseUrl) return res.status(503).json({ error: 'No voice backend configured' });
197
+ if (!isAllowedBackendUrl(cfg.baseUrl)) return res.status(400).json({ error: 'Invalid voice backend URL.' });
198
+ const text = req.body?.text;
199
+ if (typeof text !== 'string' || !text.trim()) return res.status(400).json({ error: 'text required' });
200
+ try {
201
+ const r = await fetchWithTimeout(`${cfg.baseUrl}/audio/speech`, {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json', ...authHeader(cfg.apiKey) },
204
+ body: JSON.stringify({
205
+ model: cfg.ttsModel,
206
+ voice: cfg.ttsVoice,
207
+ input: text,
208
+ ...(cfg.ttsFormat ? { response_format: cfg.ttsFormat } : {}),
209
+ }),
210
+ });
211
+ if (!r.ok) {
212
+ const errText = await r.text().catch(() => 'tts failed');
213
+ return upstreamError(res, r.status, errText);
214
+ }
215
+ res.setHeader('Content-Type', r.headers.get('content-type') || 'audio/mpeg');
216
+ res.setHeader('Cache-Control', 'no-store');
217
+ if (!r.body) return res.end();
218
+ Readable.fromWeb(r.body).on('error', (error) => res.destroy(error)).pipe(res);
219
+ } catch (e) {
220
+ backendError(res, e);
221
+ }
222
+ });
223
+
224
+ export default router;