@thevinci/web 1.0.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 (337) hide show
  1. package/README.md +197 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +4887 -0
  5. package/bin/cli.test.js +64 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +528 -0
  12. package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
  13. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
  73. package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
  74. package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
  75. package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
  76. package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
  77. package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
  78. package/dist/assets/es-4o9ciP61.js +15 -0
  79. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  80. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  81. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  82. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  83. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  84. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  85. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  86. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  87. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  88. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  89. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  90. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  91. package/dist/assets/index-DLTDToSP.css +1 -0
  92. package/dist/assets/index-DgiFEKGN.js +1 -0
  93. package/dist/assets/ko-B20imCHE.js +15 -0
  94. package/dist/assets/main-BV3KOtdA.css +1 -0
  95. package/dist/assets/main-CDKJj0sH.js +226 -0
  96. package/dist/assets/main-LC-PSNVM.js +2 -0
  97. package/dist/assets/miniChat-CQUiG_cr.js +2 -0
  98. package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
  99. package/dist/assets/pl-DQJ7LSzj.js +15 -0
  100. package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
  101. package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
  102. package/dist/assets/uk-BNFxOlO4.js +15 -0
  103. package/dist/assets/vendor--DBfsbEis.css +1 -0
  104. package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
  105. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  106. package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
  107. package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
  108. package/dist/assets/worker-CbT6TVo7.js +155 -0
  109. package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
  110. package/dist/favicon-16.png +0 -0
  111. package/dist/favicon-32.png +0 -0
  112. package/dist/favicon.png +0 -0
  113. package/dist/favicon.svg +528 -0
  114. package/dist/index.html +607 -0
  115. package/dist/logo-dark-192x192.png +0 -0
  116. package/dist/logo-dark-512x512.png +0 -0
  117. package/dist/logo-dark-512x512.svg +528 -0
  118. package/dist/logo-light-192x192.png +0 -0
  119. package/dist/logo-light-512x512.png +0 -0
  120. package/dist/logo-light-512x512.svg +528 -0
  121. package/dist/mini-chat.html +16 -0
  122. package/dist/pwa-192.png +0 -0
  123. package/dist/pwa-512.png +0 -0
  124. package/dist/pwa-maskable-192.png +0 -0
  125. package/dist/pwa-maskable-512.png +0 -0
  126. package/dist/site.webmanifest +21 -0
  127. package/dist/sw.js +1 -0
  128. package/package.json +118 -0
  129. package/public/apple-touch-icon-120x120.png +0 -0
  130. package/public/apple-touch-icon-152x152.png +0 -0
  131. package/public/apple-touch-icon-167x167.png +0 -0
  132. package/public/apple-touch-icon-180x180.png +0 -0
  133. package/public/apple-touch-icon.png +0 -0
  134. package/public/apple-touch-icon.svg +528 -0
  135. package/public/favicon-16.png +0 -0
  136. package/public/favicon-32.png +0 -0
  137. package/public/favicon.png +0 -0
  138. package/public/favicon.svg +528 -0
  139. package/public/logo-dark-192x192.png +0 -0
  140. package/public/logo-dark-512x512.png +0 -0
  141. package/public/logo-dark-512x512.svg +528 -0
  142. package/public/logo-light-192x192.png +0 -0
  143. package/public/logo-light-512x512.png +0 -0
  144. package/public/logo-light-512x512.svg +528 -0
  145. package/public/pwa-192.png +0 -0
  146. package/public/pwa-512.png +0 -0
  147. package/public/pwa-maskable-192.png +0 -0
  148. package/public/pwa-maskable-512.png +0 -0
  149. package/public/site.webmanifest +21 -0
  150. package/server/TERMINAL_WS_PROTOCOL.md +48 -0
  151. package/server/index.d.ts +39 -0
  152. package/server/index.js +1311 -0
  153. package/server/lib/cloudflare-tunnel.js +650 -0
  154. package/server/lib/event-stream/DOCUMENTATION.md +61 -0
  155. package/server/lib/event-stream/directory-ws-bridge.js +185 -0
  156. package/server/lib/event-stream/global-hub.js +158 -0
  157. package/server/lib/event-stream/global-hub.test.js +140 -0
  158. package/server/lib/event-stream/global-ws-bridge.js +206 -0
  159. package/server/lib/event-stream/index.js +25 -0
  160. package/server/lib/event-stream/protocol.js +131 -0
  161. package/server/lib/event-stream/protocol.test.js +182 -0
  162. package/server/lib/event-stream/runtime.js +180 -0
  163. package/server/lib/event-stream/runtime.test.js +512 -0
  164. package/server/lib/event-stream/upstream-reader.js +226 -0
  165. package/server/lib/event-stream/upstream-reader.test.js +276 -0
  166. package/server/lib/fs/DOCUMENTATION.md +36 -0
  167. package/server/lib/fs/routes.js +1040 -0
  168. package/server/lib/fs/search.js +238 -0
  169. package/server/lib/git/DOCUMENTATION.md +152 -0
  170. package/server/lib/git/credentials.js +74 -0
  171. package/server/lib/git/identity-storage.js +112 -0
  172. package/server/lib/git/index.js +6 -0
  173. package/server/lib/git/routes.js +972 -0
  174. package/server/lib/git/service.js +3432 -0
  175. package/server/lib/git/service.test.js +39 -0
  176. package/server/lib/github/DOCUMENTATION.md +171 -0
  177. package/server/lib/github/auth.js +307 -0
  178. package/server/lib/github/device-flow.js +50 -0
  179. package/server/lib/github/index.js +24 -0
  180. package/server/lib/github/octokit.js +10 -0
  181. package/server/lib/github/pr-status.js +519 -0
  182. package/server/lib/github/repo/fork-detection.js +102 -0
  183. package/server/lib/github/repo/index.js +55 -0
  184. package/server/lib/github/routes.js +1560 -0
  185. package/server/lib/magic-prompts/routes.js +63 -0
  186. package/server/lib/magic-prompts/runtime.js +119 -0
  187. package/server/lib/notifications/DOCUMENTATION.md +122 -0
  188. package/server/lib/notifications/emitter-runtime.js +102 -0
  189. package/server/lib/notifications/index.js +4 -0
  190. package/server/lib/notifications/message.js +52 -0
  191. package/server/lib/notifications/message.test.js +34 -0
  192. package/server/lib/notifications/push-runtime.js +304 -0
  193. package/server/lib/notifications/routes.js +315 -0
  194. package/server/lib/notifications/runtime.js +566 -0
  195. package/server/lib/notifications/template-runtime.js +349 -0
  196. package/server/lib/notifications/template-runtime.test.js +26 -0
  197. package/server/lib/opencode/DOCUMENTATION.md +362 -0
  198. package/server/lib/opencode/agents.js +634 -0
  199. package/server/lib/opencode/auth-state-runtime.js +88 -0
  200. package/server/lib/opencode/auth.js +83 -0
  201. package/server/lib/opencode/bootstrap-runtime.js +131 -0
  202. package/server/lib/opencode/cli-entry-runtime.js +43 -0
  203. package/server/lib/opencode/cli-options.js +128 -0
  204. package/server/lib/opencode/commands.js +339 -0
  205. package/server/lib/opencode/config-entity-routes.js +370 -0
  206. package/server/lib/opencode/core-routes.js +500 -0
  207. package/server/lib/opencode/core-routes.test.js +26 -0
  208. package/server/lib/opencode/env-config.js +74 -0
  209. package/server/lib/opencode/env-keys.js +68 -0
  210. package/server/lib/opencode/env-runtime.js +1162 -0
  211. package/server/lib/opencode/env-runtime.test.js +116 -0
  212. package/server/lib/opencode/feature-routes-runtime.js +244 -0
  213. package/server/lib/opencode/hmr-state-runtime.js +85 -0
  214. package/server/lib/opencode/index.js +66 -0
  215. package/server/lib/opencode/lifecycle.js +1019 -0
  216. package/server/lib/opencode/lifecycle.test.js +240 -0
  217. package/server/lib/opencode/mcp.js +278 -0
  218. package/server/lib/opencode/network-runtime.js +104 -0
  219. package/server/lib/opencode/network-runtime.test.js +37 -0
  220. package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
  221. package/server/lib/opencode/path-utils.js +100 -0
  222. package/server/lib/opencode/path-utils.test.js +71 -0
  223. package/server/lib/opencode/project-directory-runtime.js +124 -0
  224. package/server/lib/opencode/project-icon-routes.js +399 -0
  225. package/server/lib/opencode/project-icon-routes.test.js +107 -0
  226. package/server/lib/opencode/providers.js +96 -0
  227. package/server/lib/opencode/proxy.js +445 -0
  228. package/server/lib/opencode/pwa-manifest-routes.js +257 -0
  229. package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
  230. package/server/lib/opencode/routes.js +541 -0
  231. package/server/lib/opencode/server-startup-runtime.js +156 -0
  232. package/server/lib/opencode/server-utils-runtime.js +168 -0
  233. package/server/lib/opencode/server-utils-runtime.test.js +135 -0
  234. package/server/lib/opencode/session-runtime.js +356 -0
  235. package/server/lib/opencode/session-runtime.test.js +151 -0
  236. package/server/lib/opencode/settings-helpers.js +770 -0
  237. package/server/lib/opencode/settings-helpers.test.js +109 -0
  238. package/server/lib/opencode/settings-normalization-runtime.js +428 -0
  239. package/server/lib/opencode/settings-runtime.js +826 -0
  240. package/server/lib/opencode/settings-runtime.test.js +85 -0
  241. package/server/lib/opencode/shared.js +615 -0
  242. package/server/lib/opencode/shutdown-runtime.js +139 -0
  243. package/server/lib/opencode/shutdown-runtime.test.js +58 -0
  244. package/server/lib/opencode/skill-routes.js +701 -0
  245. package/server/lib/opencode/skills.js +548 -0
  246. package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
  247. package/server/lib/opencode/static-routes-runtime.js +65 -0
  248. package/server/lib/opencode/theme-runtime.js +167 -0
  249. package/server/lib/opencode/tunnel-auth.js +591 -0
  250. package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
  251. package/server/lib/opencode/vinci-routes.js +76 -0
  252. package/server/lib/opencode/watcher.js +115 -0
  253. package/server/lib/opencode/watcher.test.js +239 -0
  254. package/server/lib/preview/proxy-runtime.js +1333 -0
  255. package/server/lib/preview/proxy-runtime.test.js +144 -0
  256. package/server/lib/projects/project-config.js +567 -0
  257. package/server/lib/projects/project-config.test.js +175 -0
  258. package/server/lib/projects/project-id.js +13 -0
  259. package/server/lib/quota/DOCUMENTATION.md +58 -0
  260. package/server/lib/quota/index.js +25 -0
  261. package/server/lib/quota/providers/claude.js +107 -0
  262. package/server/lib/quota/providers/codex.js +113 -0
  263. package/server/lib/quota/providers/copilot.js +165 -0
  264. package/server/lib/quota/providers/google/api.js +92 -0
  265. package/server/lib/quota/providers/google/auth.js +108 -0
  266. package/server/lib/quota/providers/google/index.js +124 -0
  267. package/server/lib/quota/providers/google/transforms.js +109 -0
  268. package/server/lib/quota/providers/index.js +168 -0
  269. package/server/lib/quota/providers/interface.js +55 -0
  270. package/server/lib/quota/providers/kimi.js +108 -0
  271. package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
  272. package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
  273. package/server/lib/quota/providers/nanogpt.js +124 -0
  274. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  275. package/server/lib/quota/providers/openai.js +91 -0
  276. package/server/lib/quota/providers/openrouter.js +92 -0
  277. package/server/lib/quota/providers/zai.js +91 -0
  278. package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
  279. package/server/lib/quota/providers/zhipuai.js +114 -0
  280. package/server/lib/quota/routes.js +27 -0
  281. package/server/lib/quota/utils/auth.js +50 -0
  282. package/server/lib/quota/utils/formatters.js +85 -0
  283. package/server/lib/quota/utils/formatters.test.js +54 -0
  284. package/server/lib/quota/utils/index.js +10 -0
  285. package/server/lib/quota/utils/transformers.js +55 -0
  286. package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
  287. package/server/lib/scheduled-tasks/routes.js +235 -0
  288. package/server/lib/scheduled-tasks/runtime.js +773 -0
  289. package/server/lib/scheduled-tasks/runtime.test.js +100 -0
  290. package/server/lib/security/request-security.js +115 -0
  291. package/server/lib/session-folders/routes.js +63 -0
  292. package/server/lib/session-folders/routes.test.js +102 -0
  293. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  294. package/server/lib/skills-catalog/cache.js +29 -0
  295. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  296. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  297. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  298. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  299. package/server/lib/skills-catalog/curated-sources.js +21 -0
  300. package/server/lib/skills-catalog/git.js +77 -0
  301. package/server/lib/skills-catalog/index.js +42 -0
  302. package/server/lib/skills-catalog/install.js +294 -0
  303. package/server/lib/skills-catalog/scan.js +221 -0
  304. package/server/lib/skills-catalog/source.js +87 -0
  305. package/server/lib/terminal/DOCUMENTATION.md +76 -0
  306. package/server/lib/terminal/index.js +31 -0
  307. package/server/lib/terminal/output-replay-buffer.js +78 -0
  308. package/server/lib/terminal/output-replay-buffer.test.js +75 -0
  309. package/server/lib/terminal/runtime.js +850 -0
  310. package/server/lib/terminal/runtime.test.js +96 -0
  311. package/server/lib/terminal/terminal-ws-protocol.js +68 -0
  312. package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
  313. package/server/lib/text/DOCUMENTATION.md +35 -0
  314. package/server/lib/text/summarization.js +138 -0
  315. package/server/lib/text/summarization.test.js +34 -0
  316. package/server/lib/tts/DOCUMENTATION.md +146 -0
  317. package/server/lib/tts/base-url.js +62 -0
  318. package/server/lib/tts/capability-runtime.js +31 -0
  319. package/server/lib/tts/index.js +19 -0
  320. package/server/lib/tts/routes.js +261 -0
  321. package/server/lib/tts/routes.test.js +53 -0
  322. package/server/lib/tts/service.js +178 -0
  323. package/server/lib/tts/stt.js +75 -0
  324. package/server/lib/tunnels/DOCUMENTATION.md +18 -0
  325. package/server/lib/tunnels/index.js +166 -0
  326. package/server/lib/tunnels/managed-config.js +201 -0
  327. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  328. package/server/lib/tunnels/registry.js +51 -0
  329. package/server/lib/tunnels/routes.js +605 -0
  330. package/server/lib/tunnels/types.js +219 -0
  331. package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
  332. package/server/lib/ui-auth/ui-auth.js +673 -0
  333. package/server/lib/ui-auth/ui-passkeys.js +545 -0
  334. package/server/opencode-proxy.test.js +151 -0
  335. package/server/proxy-headers.js +61 -0
  336. package/server/proxy-headers.test.js +58 -0
  337. package/server/sse-routes.test.js +152 -0
@@ -0,0 +1,62 @@
1
+ const LOCAL_BASE_URL_HOSTS = new Set([
2
+ 'localhost',
3
+ '127.0.0.1',
4
+ '::1',
5
+ 'host.docker.internal',
6
+ ]);
7
+
8
+ const isEnvFlagEnabled = (value) => {
9
+ if (value === true || value === 1) return true;
10
+ if (typeof value !== 'string') return false;
11
+ const normalized = value.trim().toLowerCase();
12
+ return normalized === '1' || normalized === 'true';
13
+ };
14
+
15
+ const normalizeHostname = (hostname) => {
16
+ if (typeof hostname !== 'string') return '';
17
+ const trimmed = hostname.trim().toLowerCase();
18
+ if (!trimmed) return '';
19
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
20
+ return trimmed.slice(1, -1);
21
+ }
22
+ return trimmed;
23
+ };
24
+
25
+ const isAllowedLocalHost = (hostname) => {
26
+ const normalized = normalizeHostname(hostname);
27
+ return LOCAL_BASE_URL_HOSTS.has(normalized);
28
+ };
29
+
30
+ export const normalizeCustomOpenAIBaseURL = (value) => {
31
+ if (typeof value !== 'string' || !value.trim()) {
32
+ return { value: undefined };
33
+ }
34
+
35
+ let parsed;
36
+ try {
37
+ parsed = new URL(value.trim());
38
+ } catch {
39
+ return { error: 'Custom server URL is invalid' };
40
+ }
41
+
42
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
43
+ return { error: 'Custom server URL must use http or https' };
44
+ }
45
+
46
+ if (parsed.username || parsed.password) {
47
+ return { error: 'Custom server URL must not include credentials' };
48
+ }
49
+
50
+ const allowRemote = isEnvFlagEnabled(process.env.VINCI_ALLOW_REMOTE_OPENAI_COMPAT_URLS);
51
+ if (!allowRemote && !isAllowedLocalHost(parsed.hostname)) {
52
+ return {
53
+ error: 'Remote custom server URLs are disabled. Set VINCI_ALLOW_REMOTE_OPENAI_COMPAT_URLS=true to allow this host.',
54
+ };
55
+ }
56
+
57
+ parsed.hash = '';
58
+ parsed.search = '';
59
+ const pathname = parsed.pathname.replace(/\/+$/, '');
60
+ const normalizedPath = pathname.length > 0 ? pathname : '';
61
+ return { value: `${parsed.protocol}//${parsed.host}${normalizedPath}` };
62
+ };
@@ -0,0 +1,31 @@
1
+ export const detectSayTtsCapability = async (processLike) => {
2
+ let sayTTSCapability = { available: false, voices: [], reason: 'Not checked' };
3
+
4
+ if (processLike.platform === 'darwin') {
5
+ try {
6
+ const { exec } = await import('child_process');
7
+ const { promisify } = await import('util');
8
+ const execAsync = promisify(exec);
9
+ const { stdout } = await execAsync('say -v "?"');
10
+ const voices = stdout.split('\n')
11
+ .filter((line) => line.trim())
12
+ .map((line) => {
13
+ const match = line.match(/^(.+?)\s+([a-zA-Z]{2}_[a-zA-Z]{2,3})\s+#/);
14
+ if (match) {
15
+ return { name: match[1].trim(), locale: match[2] };
16
+ }
17
+ return null;
18
+ })
19
+ .filter(Boolean);
20
+ sayTTSCapability = { available: true, voices };
21
+ console.log(`macOS Say TTS available with ${voices.length} voices`);
22
+ } catch (error) {
23
+ sayTTSCapability = { available: false, voices: [], reason: 'say command not available' };
24
+ console.log('macOS Say TTS not available:', error.message);
25
+ }
26
+ } else {
27
+ sayTTSCapability = { available: false, voices: [], reason: 'Not macOS' };
28
+ }
29
+
30
+ return sayTTSCapability;
31
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * TTS Module Entry Point
3
+ *
4
+ * Public export surface for the Text-to-Speech domain module.
5
+ */
6
+
7
+ export {
8
+ ttsService,
9
+ TTSService,
10
+ TTS_VOICES,
11
+ } from './service.js';
12
+
13
+ export {
14
+ summarizeText,
15
+ sanitizeForTTS,
16
+ sanitizeForNote,
17
+ } from '../text/summarization.js';
18
+
19
+ export { transcribeAudio } from './stt.js';
@@ -0,0 +1,261 @@
1
+ import express from 'express';
2
+ import { normalizeCustomOpenAIBaseURL } from './base-url.js';
3
+ import { summarizeText, sanitizeForTTS, sanitizeForNote } from '../text/summarization.js';
4
+
5
+ export function registerTtsRoutes(app, { sayTTSCapability }) {
6
+ let ttsModulePromise = null;
7
+ const getTtsModule = async () => {
8
+ if (!ttsModulePromise) {
9
+ ttsModulePromise = import('./index.js');
10
+ }
11
+ return ttsModulePromise;
12
+ };
13
+
14
+ app.post('/api/voice/token', async (req, res) => {
15
+ console.log('[Voice] Token request received:', {
16
+ contentType: req.headers['content-type'] || null,
17
+ });
18
+ try {
19
+ const openaiApiKey = process.env.OPENAI_API_KEY;
20
+ console.log('[Voice] OpenAI API Key present:', !!openaiApiKey);
21
+
22
+ if (!openaiApiKey) {
23
+ return res.status(503).json({
24
+ allowed: false,
25
+ error: 'OpenAI voice service not configured. Set OPENAI_API_KEY environment variable.'
26
+ });
27
+ }
28
+
29
+ // Return success - OpenAI TTS is available
30
+ res.json({
31
+ allowed: true,
32
+ provider: 'openai',
33
+ message: 'OpenAI TTS is available'
34
+ });
35
+ } catch (error) {
36
+ console.error('[Voice] Token generation error:', error);
37
+ res.status(500).json({
38
+ allowed: false,
39
+ error: 'Voice service error'
40
+ });
41
+ }
42
+ });
43
+
44
+ // Server-side TTS endpoint - streams audio from OpenAI TTS API
45
+ app.post('/api/tts/speak', async (req, res) => {
46
+ try {
47
+ const { text, voice = 'nova', model = 'gpt-4o-mini-tts', speed = 0.9, instructions, providerId, modelId, apiKey, baseURL } = req.body || {};
48
+
49
+ const normalizedBaseURLResult = normalizeCustomOpenAIBaseURL(baseURL);
50
+ if (normalizedBaseURLResult.error) {
51
+ return res.status(400).json({ error: normalizedBaseURLResult.error });
52
+ }
53
+ const normalizedBaseURL = normalizedBaseURLResult.value;
54
+
55
+ console.log('[TTS] Request received:', { voice, model, speed, textLength: text?.length, hasApiKey: !!apiKey, hasBaseURL: !!baseURL });
56
+
57
+ if (!text || typeof text !== 'string' || !text.trim()) {
58
+ return res.status(400).json({ error: 'Text is required' });
59
+ }
60
+
61
+ // Dynamically import the TTS service (ESM)
62
+ const { ttsService } = await getTtsModule();
63
+
64
+ // Check availability - server-configured key, client-provided key, or custom server URL
65
+ const hasServerKey = ttsService.isAvailable();
66
+ const hasClientKey = apiKey && typeof apiKey === 'string' && apiKey.trim().length > 0;
67
+ const hasCustomBaseURL = typeof normalizedBaseURL === 'string' && normalizedBaseURL.length > 0;
68
+
69
+ if (!hasServerKey && !hasClientKey && !hasCustomBaseURL) {
70
+ return res.status(503).json({
71
+ error: 'TTS service not available. Please configure OpenAI in OpenCode, provide an API key, or set a custom server URL in settings.'
72
+ });
73
+ }
74
+
75
+ let textToSpeak = text.trim();
76
+
77
+ // Historical summarize request fields are intentionally ignored. The
78
+ // model-backed summarization provider is retired.
79
+
80
+ const result = await ttsService.generateSpeechStream({
81
+ text: textToSpeak,
82
+ voice,
83
+ model,
84
+ speed,
85
+ instructions,
86
+ apiKey: hasClientKey ? apiKey.trim() : undefined,
87
+ baseURL: hasCustomBaseURL ? normalizedBaseURL : undefined,
88
+ });
89
+
90
+ res.setHeader('Content-Type', result.contentType);
91
+ res.setHeader('Cache-Control', 'no-cache');
92
+ res.setHeader('Content-Length', result.buffer.length);
93
+ res.send(result.buffer);
94
+ } catch (error) {
95
+ console.error('[TTS] Error:', error);
96
+ if (!res.headersSent) {
97
+ const { model: m, voice: v, baseURL: b } = req.body || {};
98
+ res.status(500).json({
99
+ error: error instanceof Error ? error.message : 'TTS generation failed',
100
+ detail: { model: m, voice: v, hasBaseURL: !!b },
101
+ });
102
+ }
103
+ }
104
+ });
105
+
106
+ app.post('/api/text/summarize', async (req, res) => {
107
+ try {
108
+ const { text, threshold = 200, maxLength = 500, mode } = req.body || {};
109
+
110
+ if (!text || typeof text !== 'string' || !text.trim()) {
111
+ return res.status(400).json({ error: 'Text is required' });
112
+ }
113
+
114
+ const result = await summarizeText({
115
+ text,
116
+ threshold,
117
+ maxLength,
118
+ mode: typeof mode === 'string' ? mode : 'tts',
119
+ });
120
+
121
+ return res.json(result);
122
+ } catch (error) {
123
+ console.error('[Summarize] Error:', error);
124
+ const sanitized = typeof req.body?.mode === 'string' && req.body.mode === 'note'
125
+ ? sanitizeForNote(req.body?.text || '')
126
+ : sanitizeForTTS(req.body?.text || '');
127
+ return res.json({ summary: sanitized, summarized: false, reason: error.message });
128
+ }
129
+ });
130
+
131
+
132
+ // TTS status endpoint
133
+ app.get('/api/tts/status', async (_req, res) => {
134
+ try {
135
+ const { ttsService } = await getTtsModule();
136
+ res.json({
137
+ available: ttsService.isAvailable(),
138
+ voices: [
139
+ 'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable',
140
+ 'nova', 'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar'
141
+ ]
142
+ });
143
+ } catch (error) {
144
+ res.status(500).json({ error: 'Failed to check TTS status' });
145
+ }
146
+ });
147
+
148
+ // macOS 'say' command TTS status endpoint - returns cached capability from startup
149
+ app.get('/api/tts/say/status', (_req, res) => {
150
+ res.json(sayTTSCapability);
151
+ });
152
+
153
+ // macOS 'say' command TTS speak endpoint
154
+ app.post('/api/tts/say/speak', async (req, res) => {
155
+ try {
156
+ const { text, voice = 'Samantha', rate = 200 } = req.body || {};
157
+
158
+ if (!text || typeof text !== 'string' || !text.trim()) {
159
+ return res.status(400).json({ error: 'Text is required' });
160
+ }
161
+
162
+ // Check if we're on macOS
163
+ if (process.platform !== 'darwin') {
164
+ return res.status(503).json({ error: 'macOS say command not available on this platform' });
165
+ }
166
+
167
+ const { exec } = await import('child_process');
168
+ const { promisify } = await import('util');
169
+ const fs = await import('fs');
170
+ const os = await import('os');
171
+ const path = await import('path');
172
+ const execAsync = promisify(exec);
173
+
174
+ // Create temp file for audio output (use m4a for browser compatibility)
175
+ const tempDir = os.tmpdir();
176
+ const tempFile = path.join(tempDir, `say-${Date.now()}.m4a`);
177
+
178
+ // Escape text for shell - escape both single quotes and double quotes
179
+ const escapedText = text.trim().replace(/'/g, "'\\''").replace(/"/g, '\\"');
180
+
181
+ // Generate audio file using 'say' command
182
+ // -o outputs to file, -r sets rate (words per minute)
183
+ // --data-format=aac outputs as m4a which browsers can decode
184
+ const cmd = `say -v "${voice}" -r ${rate} -o "${tempFile}" --data-format=aac '${escapedText}'`;
185
+ console.log('[TTS-Say] Generating speech:', { textLength: text.length, voice, rate });
186
+
187
+ await execAsync(cmd);
188
+
189
+ // Read the generated audio file
190
+ const audioBuffer = await fs.promises.readFile(tempFile);
191
+
192
+ // Clean up temp file
193
+ fs.promises.unlink(tempFile).catch(() => {});
194
+
195
+ // Send audio response
196
+ res.setHeader('Content-Type', 'audio/mp4');
197
+ res.setHeader('Content-Length', audioBuffer.length);
198
+ res.send(audioBuffer);
199
+
200
+ } catch (error) {
201
+ console.error('[TTS-Say] Error:', error);
202
+ res.status(500).json({
203
+ error: error instanceof Error ? error.message : 'Say command failed'
204
+ });
205
+ }
206
+ });
207
+
208
+ // Server-side STT: receive raw audio, proxy to OpenAI-compatible transcription endpoint
209
+ app.post(
210
+ '/api/stt/transcribe',
211
+ express.raw({ type: (req) => (req.headers['content-type'] || '').startsWith('audio/'), limit: '20mb' }),
212
+ async (req, res) => {
213
+ try {
214
+ const { transcribeAudio } = await import('./stt.js');
215
+
216
+ const mimeType = (req.headers['content-type'] || 'audio/webm').split(',')[0].trim();
217
+ const baseURL = typeof req.headers['x-base-url'] === 'string' ? req.headers['x-base-url'].trim() : '';
218
+ const model = typeof req.headers['x-model'] === 'string' && req.headers['x-model'].trim().length > 0
219
+ ? req.headers['x-model'].trim()
220
+ : 'deepdml/faster-whisper-large-v3-turbo-ct2';
221
+ const language = typeof req.headers['x-language'] === 'string' && req.headers['x-language'].trim().length > 0
222
+ ? req.headers['x-language'].trim()
223
+ : undefined;
224
+
225
+ if (!req.body || !Buffer.isBuffer(req.body) || req.body.length === 0) {
226
+ return res.status(400).json({ error: 'Audio data is required' });
227
+ }
228
+
229
+ if (!baseURL) {
230
+ return res.status(400).json({ error: 'X-Base-URL header is required' });
231
+ }
232
+
233
+ console.log('[STT] Transcribing audio:', {
234
+ bytes: req.body.length,
235
+ mimeType,
236
+ model,
237
+ baseURL,
238
+ language,
239
+ });
240
+
241
+ const transcript = await transcribeAudio({
242
+ audioBuffer: req.body,
243
+ mimeType,
244
+ model,
245
+ baseURL,
246
+ language,
247
+ });
248
+
249
+ console.log('[STT] Transcript:', transcript?.slice(0, 120));
250
+ res.json({ transcript: transcript ?? '' });
251
+ } catch (error) {
252
+ console.error('[STT] Error:', error);
253
+ if (!res.headersSent) {
254
+ res.status(500).json({
255
+ error: error instanceof Error ? error.message : 'Transcription failed',
256
+ });
257
+ }
258
+ }
259
+ }
260
+ );
261
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+
5
+ import { registerTtsRoutes } from './routes.js';
6
+
7
+ const createApp = () => {
8
+ const app = express();
9
+ app.use(express.json());
10
+ registerTtsRoutes(app, {
11
+ resolveZenModel: async () => 'gpt-5-nano',
12
+ sayTTSCapability: null,
13
+ });
14
+ return app;
15
+ };
16
+
17
+ describe('tts routes', () => {
18
+ it('returns local note fallback while model summarization is retired', async () => {
19
+ const response = await request(createApp())
20
+ .post('/api/text/summarize')
21
+ .send({
22
+ text: 'First sentence. Second sentence with the useful insight.',
23
+ threshold: 0,
24
+ maxLength: 100,
25
+ mode: 'note',
26
+ });
27
+
28
+ expect(response.status).toBe(200);
29
+ expect(response.body).toMatchObject({
30
+ summary: 'First sentence.',
31
+ summarized: false,
32
+ reason: 'Model summarization provider unavailable',
33
+ });
34
+ });
35
+
36
+ it('keeps notification fallback behavior without calling zen', async () => {
37
+ const response = await request(createApp())
38
+ .post('/api/text/summarize')
39
+ .send({
40
+ text: 'Notification text that should fall back cleanly.',
41
+ threshold: 0,
42
+ maxLength: 100,
43
+ mode: 'notification',
44
+ });
45
+
46
+ expect(response.status).toBe(200);
47
+ expect(response.body).toMatchObject({
48
+ summary: 'Notification text that should fall back cleanly.',
49
+ summarized: false,
50
+ reason: 'Model summarization provider unavailable',
51
+ });
52
+ });
53
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Server-side Text-to-Speech Service
3
+ *
4
+ * Uses OpenAI's TTS API to generate audio on the server and stream it to clients.
5
+ * This bypasses mobile Safari's audio context restrictions.
6
+ */
7
+
8
+ import OpenAI from 'openai';
9
+ import { readAuthFile } from '../opencode/auth.js';
10
+ import { normalizeCustomOpenAIBaseURL } from './base-url.js';
11
+
12
+ // Voice options from OpenAI
13
+ export const TTS_VOICES = [
14
+ 'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable',
15
+ 'nova', 'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar'
16
+ ];
17
+
18
+ function getOpenAIApiKey() {
19
+ // First check environment variable
20
+ const envKey = process.env.OPENAI_API_KEY;
21
+ if (envKey) {
22
+ return envKey;
23
+ }
24
+
25
+ // Then check opencode auth file (same as usage tracker)
26
+ try {
27
+ const auth = readAuthFile();
28
+ // Check for openai, codex, or chatgpt aliases
29
+ const openaiAuth = auth.openai || auth.codex || auth.chatgpt;
30
+ if (openaiAuth) {
31
+ // Handle both string format (just the token) and object format
32
+ if (typeof openaiAuth === 'string') {
33
+ return openaiAuth;
34
+ }
35
+ // Try access token first (OAuth), then regular token
36
+ if (openaiAuth.access) {
37
+ return openaiAuth.access;
38
+ }
39
+ if (openaiAuth.token) {
40
+ return openaiAuth.token;
41
+ }
42
+ }
43
+ } catch (error) {
44
+ console.warn('[TTSService] Failed to read auth file:', error.message);
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ class TTSService {
51
+ constructor() {
52
+ this._client = null;
53
+ this._lastApiKey = null;
54
+ }
55
+
56
+ _getClient() {
57
+ const apiKey = getOpenAIApiKey();
58
+
59
+ // If API key changed or client doesn't exist, create new client
60
+ if (apiKey && (!this._client || this._lastApiKey !== apiKey)) {
61
+ this._client = new OpenAI({ apiKey });
62
+ this._lastApiKey = apiKey;
63
+ }
64
+
65
+ return this._client;
66
+ }
67
+
68
+ isAvailable() {
69
+ return this._getClient() !== null;
70
+ }
71
+
72
+ /**
73
+ * Generate speech and return as a stream
74
+ */
75
+ async generateSpeechStream(options) {
76
+ const {
77
+ text,
78
+ voice = 'coral',
79
+ model = 'gpt-4o-mini-tts',
80
+ speed = 1.0,
81
+ instructions,
82
+ apiKey,
83
+ baseURL,
84
+ } = options;
85
+
86
+ const normalizedBaseURLResult = normalizeCustomOpenAIBaseURL(baseURL);
87
+ if (normalizedBaseURLResult.error) {
88
+ throw new Error(normalizedBaseURLResult.error);
89
+ }
90
+ const normalizedBaseURL = normalizedBaseURLResult.value;
91
+
92
+ // Use provided API key / baseURL or fall back to configured key
93
+ let client;
94
+ if (normalizedBaseURL || apiKey) {
95
+ const clientOpts = {};
96
+ if (apiKey) clientOpts.apiKey = apiKey;
97
+ if (!apiKey) clientOpts.apiKey = 'not-required';
98
+ if (normalizedBaseURL) clientOpts.baseURL = normalizedBaseURL;
99
+ client = new OpenAI(clientOpts);
100
+ } else {
101
+ client = this._getClient();
102
+ }
103
+
104
+ if (!client) {
105
+ throw new Error('TTS service not available. Configure OpenAI in OpenCode, provide an API key, or set a custom server URL in settings.');
106
+ }
107
+
108
+ if (!text.trim()) {
109
+ throw new Error('Text is required for TTS');
110
+ }
111
+
112
+ try {
113
+ // OpenAI-compatible servers (custom baseURL) may not support `instructions`
114
+ // or `response_format`, but do support `speed`. Send the safe subset.
115
+ const speechParams = normalizedBaseURL
116
+ ? { model, voice, input: text, speed }
117
+ : {
118
+ model,
119
+ voice,
120
+ input: text,
121
+ speed,
122
+ ...(instructions && { instructions }),
123
+ response_format: 'mp3',
124
+ };
125
+
126
+ console.log('[TTSService] Generating speech — model:', model, 'voice:', voice, 'baseURL:', normalizedBaseURL ?? '(openai)');
127
+ const response = await client.audio.speech.create(speechParams);
128
+
129
+ const arrayBuffer = await response.arrayBuffer();
130
+ return {
131
+ buffer: Buffer.from(arrayBuffer),
132
+ contentType: 'audio/mpeg',
133
+ };
134
+ } catch (error) {
135
+ console.error('[TTSService] Error generating speech:', error);
136
+ throw new Error(`Failed to generate speech: ${error.message || 'Unknown error'}`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Generate speech and return as a buffer (for caching)
142
+ */
143
+ async generateSpeechBuffer(options) {
144
+ const client = this._getClient();
145
+ if (!client) {
146
+ throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable or configure OpenAI in OpenCode.');
147
+ }
148
+
149
+ const {
150
+ text,
151
+ voice = 'coral',
152
+ model = 'gpt-4o-mini-tts',
153
+ speed = 1.0,
154
+ instructions
155
+ } = options;
156
+
157
+ try {
158
+ const response = await client.audio.speech.create({
159
+ model,
160
+ voice,
161
+ input: text,
162
+ speed,
163
+ ...(instructions && { instructions }),
164
+ response_format: 'mp3',
165
+ });
166
+
167
+ const arrayBuffer = await response.arrayBuffer();
168
+ return Buffer.from(arrayBuffer);
169
+ } catch (error) {
170
+ console.error('[TTSService] Error generating speech buffer:', error);
171
+ throw error;
172
+ }
173
+ }
174
+ }
175
+
176
+ // Export singleton instance
177
+ export const ttsService = new TTSService();
178
+ export { TTSService };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Server-side Speech-to-Text Service
3
+ *
4
+ * Proxies audio to any OpenAI-compatible transcription endpoint
5
+ * (e.g. faster-whisper, whisper.cpp) using the OpenAI Node SDK.
6
+ */
7
+
8
+ import OpenAI, { toFile } from 'openai';
9
+ import { normalizeCustomOpenAIBaseURL } from './base-url.js';
10
+
11
+ /**
12
+ * Transcribe an audio buffer via an OpenAI-compatible /v1/audio/transcriptions endpoint.
13
+ *
14
+ * @param {object} opts
15
+ * @param {Buffer} opts.audioBuffer - Raw audio bytes
16
+ * @param {string} opts.mimeType - MIME type of the audio (e.g. 'audio/webm')
17
+ * @param {string} opts.model - Model name accepted by the remote server
18
+ * @param {string} [opts.baseURL] - Base URL of the compatible server (including /v1)
19
+ * @param {string} [opts.language] - Optional BCP-47 language hint (e.g. 'en')
20
+ * @returns {Promise<string>} Transcribed text
21
+ */
22
+ export async function transcribeAudio({ audioBuffer, mimeType, model, baseURL, language }) {
23
+ const normalizedBaseURLResult = normalizeCustomOpenAIBaseURL(baseURL);
24
+ if (normalizedBaseURLResult.error) {
25
+ throw new Error(normalizedBaseURLResult.error);
26
+ }
27
+
28
+ const normalizedBaseURL = normalizedBaseURLResult.value;
29
+ if (!normalizedBaseURL) {
30
+ throw new Error('Custom server URL is required');
31
+ }
32
+
33
+ const clientOpts = {
34
+ apiKey: process.env.OPENAI_API_KEY || 'not-required',
35
+ };
36
+ clientOpts.baseURL = normalizedBaseURL;
37
+
38
+ const client = new OpenAI(clientOpts);
39
+
40
+ // Derive a sensible filename extension from the MIME type so the server
41
+ // can infer the codec when it isn't explicit in the stream header.
42
+ const ext = mimeTypeToExt(mimeType);
43
+ const filename = `audio.${ext}`;
44
+
45
+ const file = await toFile(audioBuffer, filename, { type: mimeType });
46
+
47
+ const result = await client.audio.transcriptions.create({
48
+ file,
49
+ model,
50
+ response_format: 'json',
51
+ ...(language ? { language } : {}),
52
+ });
53
+
54
+ return result.text ?? '';
55
+ }
56
+
57
+ /**
58
+ * Map a MIME type to a file extension understood by Whisper servers.
59
+ * @param {string} mimeType
60
+ * @returns {string}
61
+ */
62
+ function mimeTypeToExt(mimeType) {
63
+ const type = (mimeType || '').split(';')[0].trim().toLowerCase();
64
+ const map = {
65
+ 'audio/webm': 'webm',
66
+ 'audio/ogg': 'ogg',
67
+ 'audio/wav': 'wav',
68
+ 'audio/wave': 'wav',
69
+ 'audio/mpeg': 'mp3',
70
+ 'audio/mp4': 'mp4',
71
+ 'audio/mp3': 'mp3',
72
+ 'audio/flac': 'flac',
73
+ };
74
+ return map[type] ?? 'webm';
75
+ }