titan-agent 5.4.2 → 5.5.6

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 (302) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/agent.js +9 -5
  3. package/dist/agent/agent.js.map +1 -1
  4. package/dist/agent/agentLoop.js +7 -3
  5. package/dist/agent/agentLoop.js.map +1 -1
  6. package/dist/agent/checkpoint.js +2 -2
  7. package/dist/agent/checkpoint.js.map +1 -1
  8. package/dist/agent/commandPost.js +3 -3
  9. package/dist/agent/commandPost.js.map +1 -1
  10. package/dist/agent/goalProposer.js +2 -2
  11. package/dist/agent/goalProposer.js.map +1 -1
  12. package/dist/agent/goals.js +3 -3
  13. package/dist/agent/goals.js.map +1 -1
  14. package/dist/agent/peerAdvise.js +1 -1
  15. package/dist/agent/peerAdvise.js.map +1 -1
  16. package/dist/agent/planner.js +4 -4
  17. package/dist/agent/planner.js.map +1 -1
  18. package/dist/agent/userProfile.js +2 -2
  19. package/dist/agent/userProfile.js.map +1 -1
  20. package/dist/cli/doctor.js +33 -0
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/onboard.js +4 -4
  23. package/dist/cli/onboard.js.map +1 -1
  24. package/dist/config/config.js +3 -3
  25. package/dist/config/config.js.map +1 -1
  26. package/dist/config/schema.js +8 -1
  27. package/dist/config/schema.js.map +1 -1
  28. package/dist/gateway/routes/adminRouter.js +500 -0
  29. package/dist/gateway/routes/adminRouter.js.map +1 -0
  30. package/dist/gateway/routes/agents.js +231 -0
  31. package/dist/gateway/routes/agents.js.map +1 -0
  32. package/dist/gateway/routes/agentsRouter.js +32 -0
  33. package/dist/gateway/routes/agentsRouter.js.map +1 -0
  34. package/dist/gateway/routes/checkpoints.js +41 -0
  35. package/dist/gateway/routes/checkpoints.js.map +1 -0
  36. package/dist/gateway/routes/commandPost.js +755 -0
  37. package/dist/gateway/routes/commandPost.js.map +1 -0
  38. package/dist/gateway/routes/companies.js +166 -0
  39. package/dist/gateway/routes/companies.js.map +1 -0
  40. package/dist/gateway/routes/files.js +295 -0
  41. package/dist/gateway/routes/files.js.map +1 -0
  42. package/dist/gateway/routes/hardwareRouter.js +151 -0
  43. package/dist/gateway/routes/hardwareRouter.js.map +1 -0
  44. package/dist/gateway/routes/mcpRouter.js +88 -0
  45. package/dist/gateway/routes/mcpRouter.js.map +1 -0
  46. package/dist/gateway/routes/mesh.js +464 -0
  47. package/dist/gateway/routes/mesh.js.map +1 -0
  48. package/dist/gateway/routes/metricsRouter.js +131 -0
  49. package/dist/gateway/routes/metricsRouter.js.map +1 -0
  50. package/dist/gateway/routes/organism.js +82 -0
  51. package/dist/gateway/routes/organism.js.map +1 -0
  52. package/dist/gateway/routes/paperclip.js +101 -0
  53. package/dist/gateway/routes/paperclip.js.map +1 -0
  54. package/dist/gateway/routes/sessions.js +227 -0
  55. package/dist/gateway/routes/sessions.js.map +1 -0
  56. package/dist/gateway/routes/skills.js +295 -0
  57. package/dist/gateway/routes/skills.js.map +1 -0
  58. package/dist/gateway/routes/socialRouter.js +145 -0
  59. package/dist/gateway/routes/socialRouter.js.map +1 -0
  60. package/dist/gateway/routes/systemRouter.js +220 -0
  61. package/dist/gateway/routes/systemRouter.js.map +1 -0
  62. package/dist/gateway/routes/teamsRecipes.js +297 -0
  63. package/dist/gateway/routes/teamsRecipes.js.map +1 -0
  64. package/dist/gateway/routes/tests.js +401 -0
  65. package/dist/gateway/routes/tests.js.map +1 -0
  66. package/dist/gateway/routes/traces.js +33 -0
  67. package/dist/gateway/routes/traces.js.map +1 -0
  68. package/dist/gateway/routes/voiceRouter.js +770 -0
  69. package/dist/gateway/routes/voiceRouter.js.map +1 -0
  70. package/dist/gateway/routes/watchRouter.js +131 -0
  71. package/dist/gateway/routes/watchRouter.js.map +1 -0
  72. package/dist/gateway/server.js +1179 -7379
  73. package/dist/gateway/server.js.map +1 -1
  74. package/dist/mcp/registry.js +2 -2
  75. package/dist/mcp/registry.js.map +1 -1
  76. package/dist/memory/episodic.js +2 -2
  77. package/dist/memory/episodic.js.map +1 -1
  78. package/dist/memory/learning.js +3 -3
  79. package/dist/memory/learning.js.map +1 -1
  80. package/dist/memory/memory.js +3 -3
  81. package/dist/memory/memory.js.map +1 -1
  82. package/dist/organism/drives.js +2 -2
  83. package/dist/organism/drives.js.map +1 -1
  84. package/dist/providers/errorTaxonomy.js +13 -0
  85. package/dist/providers/errorTaxonomy.js.map +1 -1
  86. package/dist/providers/ollama.js +3 -1
  87. package/dist/providers/ollama.js.map +1 -1
  88. package/dist/providers/openai_compat.js +4 -3
  89. package/dist/providers/openai_compat.js.map +1 -1
  90. package/dist/providers/router.js +13 -0
  91. package/dist/providers/router.js.map +1 -1
  92. package/dist/safety/fixOscillation.js +15 -0
  93. package/dist/safety/fixOscillation.js.map +1 -1
  94. package/dist/safety/killSwitch.js +2 -2
  95. package/dist/safety/killSwitch.js.map +1 -1
  96. package/dist/safety/selfRepair.js +7 -3
  97. package/dist/safety/selfRepair.js.map +1 -1
  98. package/dist/skills/builtin/agent_debate.js +2 -2
  99. package/dist/skills/builtin/agent_debate.js.map +1 -1
  100. package/dist/skills/builtin/apply_patch.js +3 -3
  101. package/dist/skills/builtin/apply_patch.js.map +1 -1
  102. package/dist/skills/builtin/shell.js +2 -2
  103. package/dist/skills/builtin/shell.js.map +1 -1
  104. package/dist/skills/builtin/voice_control.js +49 -0
  105. package/dist/skills/builtin/voice_control.js.map +1 -0
  106. package/dist/skills/builtin/widget_gallery.js +6 -1
  107. package/dist/skills/builtin/widget_gallery.js.map +1 -1
  108. package/dist/skills/registry.js +15 -4
  109. package/dist/skills/registry.js.map +1 -1
  110. package/dist/storage/JsonStorage.js +4 -4
  111. package/dist/storage/JsonStorage.js.map +1 -1
  112. package/dist/utils/constants.js +1 -1
  113. package/dist/utils/constants.js.map +1 -1
  114. package/dist/utils/helpers.js +3 -1
  115. package/dist/utils/helpers.js.map +1 -1
  116. package/dist/utils/lifecycle.js +86 -0
  117. package/dist/utils/lifecycle.js.map +1 -0
  118. package/dist/voice/bridge.js +136 -0
  119. package/dist/voice/bridge.js.map +1 -0
  120. package/docs/COO-MASTER-PLAN-2026-05-02.md +474 -0
  121. package/docs/HANDOFF/2026-04-29.md +141 -0
  122. package/docs/HANDOFF-2026-04-30.md +144 -0
  123. package/docs/HANDOFF-2026-05-03.md +114 -0
  124. package/docs/adr/2026-04-29-widget-pipeline-traceability.md +49 -0
  125. package/docs/agent-memory/README.md +45 -0
  126. package/docs/agent-memory/commands.md +100 -0
  127. package/docs/agent-memory/context-tree.md +101 -0
  128. package/docs/agent-memory/current-state.md +54 -0
  129. package/docs/agent-memory/decisions.md +78 -0
  130. package/docs/agent-memory/known-issues.md +76 -0
  131. package/docs/agent-memory/reflections.md +52 -0
  132. package/docs/agent-memory/skills-candidates.md +80 -0
  133. package/docs/superpowers/plans/2026-04-29-comprehensive-audit.md +256 -0
  134. package/docs/superpowers/plans/2026-04-29-comprehensive-test-plan.md +396 -0
  135. package/docs/superpowers/plans/2026-04-29-fix-all-prs.md +251 -0
  136. package/docs/superpowers/plans/2026-04-29-gitnexus-gap-remediation.md +969 -0
  137. package/package.json +5 -2
  138. package/ui/dist/assets/{AuditPanel-CM6Wg9hO.js → AuditPanel-VzSndmDN.js} +2 -2
  139. package/ui/dist/assets/{AutonomyPanel-CESx3ANg.js → AutonomyPanel-BiFouzAV.js} +2 -2
  140. package/ui/dist/assets/AutopilotPanel-fjOfM668.js +1 -0
  141. package/ui/dist/assets/{AutoresearchPanel-DR47NqT5.js → AutoresearchPanel-CVCxzAH3.js} +2 -2
  142. package/ui/dist/assets/BackupPanel-CHVTG--q.js +1 -0
  143. package/ui/dist/assets/{BrowserPanel-C15x9bLn.js → BrowserPanel-D5mvMKFU.js} +2 -2
  144. package/ui/dist/assets/CPActivity-B12mt35m.js +1 -0
  145. package/ui/dist/assets/CPAgentDetail-DsdShc-1.js +1 -0
  146. package/ui/dist/assets/CPAgents-j_7C-oQV.js +1 -0
  147. package/ui/dist/assets/CPApprovals-BShKSX9X.js +1 -0
  148. package/ui/dist/assets/CPCosts-CKPlhBDs.js +1 -0
  149. package/ui/dist/assets/CPDashboard-11c0nkxK.js +1 -0
  150. package/ui/dist/assets/CPFiles-BhLEOnXy.js +1 -0
  151. package/ui/dist/assets/CPGoals-Bi3t1b2P.js +1 -0
  152. package/ui/dist/assets/CPInbox-Bbr7khp6.js +11 -0
  153. package/ui/dist/assets/CPIssueDetail-DSdgNK8r.js +1 -0
  154. package/ui/dist/assets/CPIssues-DDEVKhX6.js +1 -0
  155. package/ui/dist/assets/CPLayout-DgPOfyGv.js +17 -0
  156. package/ui/dist/assets/CPOrg-Df73RrRJ.js +8 -0
  157. package/ui/dist/assets/CPRuns-ByioAz8w.js +1 -0
  158. package/ui/dist/assets/{CPSocial-nb-j7sOE.js → CPSocial-Dlnr_w1X.js} +2 -2
  159. package/ui/dist/assets/ChannelsPanel-DQjQCTK5.js +1 -0
  160. package/ui/dist/assets/CheckpointsPanel-C4vKjlAJ.js +1 -0
  161. package/ui/dist/assets/CommandPostHub-C9pp5Giq.js +24 -0
  162. package/ui/dist/assets/CronPanel-C6bzUfrD.js +1 -0
  163. package/ui/dist/assets/DaemonPanel-BA5Tb_UO.js +1 -0
  164. package/ui/dist/assets/{DataTable-B2Ma8hfi.js → DataTable-CH7IYJJh.js} +1 -1
  165. package/ui/dist/assets/{EmptyState-CcKyk5Yn.js → EmptyState-jU6yNDnF.js} +1 -1
  166. package/ui/dist/assets/{EvalHarnessPanel-BqtMc1ZM.js → EvalHarnessPanel-DnYqredY.js} +2 -2
  167. package/ui/dist/assets/EvalPanel-ChO7CD1r.js +1 -0
  168. package/ui/dist/assets/{FilesPanel-3QKvrWPo.js → FilesPanel-CaUkv2is.js} +2 -2
  169. package/ui/dist/assets/FleetPanel-DC_5uj0N.js +1 -0
  170. package/ui/dist/assets/{HomelabPanel-DhrjTX9m.js → HomelabPanel-CE5PGRpL.js} +2 -2
  171. package/ui/dist/assets/InfraView-C-uSlvb9.js +2 -0
  172. package/ui/dist/assets/InlineEditableField-BMQjiE6-.js +1 -0
  173. package/ui/dist/assets/Input-Bu_b3qmY.js +1 -0
  174. package/ui/dist/assets/IntegrationsPanel-DsYpAq43.js +1 -0
  175. package/ui/dist/assets/IntelligenceView-DUdIO1K7.js +2 -0
  176. package/ui/dist/assets/LearningPanel-UpQZC-mA.js +1 -0
  177. package/ui/dist/assets/LogsPanel-ClXJ4fcr.js +1 -0
  178. package/ui/dist/assets/McpPanel-JKgtIERQ.js +1 -0
  179. package/ui/dist/assets/{MemoryGraphPanel-Bzvjmzvk.js → MemoryGraphPanel-Bo2OrvA6.js} +2 -2
  180. package/ui/dist/assets/MemoryWikiPanel-BqJ1AmYm.js +11 -0
  181. package/ui/dist/assets/{MeshPanel-C3LJSlht.js → MeshPanel-BJVGYvwk.js} +2 -2
  182. package/ui/dist/assets/Modal-CAAooiZU.js +1 -0
  183. package/ui/dist/assets/NvidiaPanel-BtCg3G4w.js +1 -0
  184. package/ui/dist/assets/OrganismPanel-DgrTTzcF.js +1 -0
  185. package/ui/dist/assets/OverviewPanel-rVav1Hox.js +1 -0
  186. package/ui/dist/assets/{PageHeader-BimceqQo.js → PageHeader-CnZtP8ek.js} +1 -1
  187. package/ui/dist/assets/PaperclipPanel-C-FKdhiF.js +1 -0
  188. package/ui/dist/assets/{PersonasPanel-L1j78p6H.js → PersonasPanel-BmlxokfB.js} +1 -1
  189. package/ui/dist/assets/RecipesPanel-BNKKChis.js +1 -0
  190. package/ui/dist/assets/SecurityPanel-I7JRHiNy.js +1 -0
  191. package/ui/dist/assets/SelfImprovePanel-u9h0Lt3p.js +1 -0
  192. package/ui/dist/assets/{SelfProposalsPanel-lNmiDThB.js → SelfProposalsPanel-DKl9iBjM.js} +2 -2
  193. package/ui/dist/assets/SessionsPanel-BhRiWI_g.js +1 -0
  194. package/ui/dist/assets/{SessionsTab-JQbltWww.js → SessionsTab-Bk08wyeY.js} +1 -1
  195. package/ui/dist/assets/SettingsPanel-haLfmG2k.js +1 -0
  196. package/ui/dist/assets/SettingsView--gi3fxI8.js +2 -0
  197. package/ui/dist/assets/{SkeletonLoader-atQtpcF5.js → SkeletonLoader-B5v09EF_.js} +1 -1
  198. package/ui/dist/assets/{SkillsPanel-DlFs2ih7.js → SkillsPanel-BlAHFLcQ.js} +1 -1
  199. package/ui/dist/assets/SomaView-CExtS3zw.js +5 -0
  200. package/ui/dist/assets/{StatCard-DciE_Iqc.js → StatCard-BIsyMybM.js} +1 -1
  201. package/ui/dist/assets/{StatusBadge-BtfSPoW2.js → StatusBadge-D5nU7El8.js} +1 -1
  202. package/ui/dist/assets/Tabs-BBYZrBI8.js +1 -0
  203. package/ui/dist/assets/TeamsPanel-LPXJg823.js +1 -0
  204. package/ui/dist/assets/TelemetryPanel-EqpRBmOI.js +1 -0
  205. package/ui/dist/assets/TitanCanvas-BCbWnLMd.js +985 -0
  206. package/ui/dist/assets/ToolsView-CeP0Zz-N.js +2 -0
  207. package/ui/dist/assets/{Tooltip-70UK0E2I.js → Tooltip-BSO2XVpF.js} +1 -1
  208. package/ui/dist/assets/TraceViewer-BKI7o5B0.js +1 -0
  209. package/ui/dist/assets/TrainingPanel-c-RhjdE1.js +1 -0
  210. package/ui/dist/assets/VoiceOverlay-D-gc58b0.js +27 -0
  211. package/ui/dist/assets/VramPanel-C6xc7zgd.js +1 -0
  212. package/ui/dist/assets/{WatchView-C-sGFpVy.js → WatchView-dqBVCVH0.js} +1 -1
  213. package/ui/dist/assets/WorkTab-CBoLNrTM.js +1 -0
  214. package/ui/dist/assets/{WorkflowsPanel-CvgQU1xI.js → WorkflowsPanel-BAnSTOYe.js} +2 -2
  215. package/ui/dist/assets/approvalHeadline-DB9SgR-9.js +1 -0
  216. package/ui/dist/assets/{arrow-left-DwqHtJiU.js → arrow-left-5chqas7J.js} +1 -1
  217. package/ui/dist/assets/briefcase-D4vLzudp.js +6 -0
  218. package/ui/dist/assets/{chart-column-BtNO6sRy.js → chart-column-CdFlBpoP.js} +1 -1
  219. package/ui/dist/assets/check-Bpm1IONe.js +6 -0
  220. package/ui/dist/assets/chevron-down-D7OLjvuD.js +6 -0
  221. package/ui/dist/assets/chevron-right-aQEw2mUW.js +6 -0
  222. package/ui/dist/assets/chevron-up-C5g6pEj8.js +6 -0
  223. package/ui/dist/assets/{circle-check-big-DZRE_MbN.js → circle-check-big-fPhEdP88.js} +1 -1
  224. package/ui/dist/assets/clock-CTsgP_Sn.js +6 -0
  225. package/ui/dist/assets/{dollar-sign-aVG3a5eL.js → dollar-sign-CudFVYFc.js} +1 -1
  226. package/ui/dist/assets/{download-BxiWJU4G.js → download-DZRxDn67.js} +1 -1
  227. package/ui/dist/assets/external-link-BZ0y_Ahx.js +6 -0
  228. package/ui/dist/assets/{eye-off-CkgfFYhm.js → eye-off-BmJF0YYx.js} +1 -1
  229. package/ui/dist/assets/folder-DA43TRCm.js +11 -0
  230. package/ui/dist/assets/{funnel-PkLdxKyC.js → funnel-J3mULzrz.js} +1 -1
  231. package/ui/dist/assets/{git-branch-BM-Gw95X.js → git-branch-oHibJqDq.js} +1 -1
  232. package/ui/dist/assets/{index-D0RJ8701.css → index-BR0vfkIi.css} +1 -1
  233. package/ui/dist/assets/{index-CahJbWSR.js → index-DzwowwSI.js} +20 -20
  234. package/ui/dist/assets/{layers-BuGf4FIJ.js → layers-DsyEyu7z.js} +1 -1
  235. package/ui/dist/assets/{legacy-CR6o4t-y.js → legacy-8ITl64sV.js} +1 -1
  236. package/ui/dist/assets/{lightbulb-n8gc_XAL.js → lightbulb-C54Ske-p.js} +1 -1
  237. package/ui/dist/assets/list-todo-Cnd4rdoK.js +6 -0
  238. package/ui/dist/assets/loader-circle-1YOBsoQp.js +6 -0
  239. package/ui/dist/assets/network-DbGDAdrn.js +6 -0
  240. package/ui/dist/assets/{pause-DCV52koX.js → pause-CYhO_uQo.js} +1 -1
  241. package/ui/dist/assets/{play-CcJ9BnCh.js → play-DVY9c5Ck.js} +1 -1
  242. package/ui/dist/assets/{plug-CfWBXfCl.js → plug-BcXjlPUL.js} +1 -1
  243. package/ui/dist/assets/plus-Csu2v9GN.js +6 -0
  244. package/ui/dist/assets/{proxy-CzZDfLmm.js → proxy-DxS2_9D7.js} +1 -1
  245. package/ui/dist/assets/rotate-ccw-Co-_W04j.js +6 -0
  246. package/ui/dist/assets/save-Btx-kpoW.js +6 -0
  247. package/ui/dist/assets/search-0hXTwEZR.js +6 -0
  248. package/ui/dist/assets/send-TEpapzQR.js +6 -0
  249. package/ui/dist/assets/shield-check-DjBJXZUr.js +6 -0
  250. package/ui/dist/assets/{square-DJpUhlxi.js → square-OweUvjP-.js} +1 -1
  251. package/ui/dist/assets/{target-DWcmM_9m.js → target-BRW80Xer.js} +1 -1
  252. package/ui/dist/assets/terminal-BtiqJ628.js +16 -0
  253. package/ui/dist/assets/{toggle-right-YusFQ69L.js → toggle-right-CKtSrl28.js} +1 -1
  254. package/ui/dist/assets/{trash-2-CK7yQ55V.js → trash-2-DgWrHVax.js} +1 -1
  255. package/ui/dist/assets/{trending-up-DGjFyubC.js → trending-up-MpIrE4j6.js} +1 -1
  256. package/ui/dist/assets/{trophy-uQv_NgDB.js → trophy-CECuZNhX.js} +1 -1
  257. package/ui/dist/assets/users-dZgv4ePG.js +16 -0
  258. package/ui/dist/assets/wrench-CDz3xYve.js +11 -0
  259. package/ui/dist/index.html +2 -2
  260. package/ui/dist/assets/AutopilotPanel-DtEet1hJ.js +0 -1
  261. package/ui/dist/assets/BackupPanel-BGP8p3l3.js +0 -1
  262. package/ui/dist/assets/CPAgents-DYUtPzSq.js +0 -1
  263. package/ui/dist/assets/CPDashboard-Bf0-SyCh.js +0 -6
  264. package/ui/dist/assets/CPFiles-CxgxjQcO.js +0 -1
  265. package/ui/dist/assets/CPGoals-BsmCMTvT.js +0 -1
  266. package/ui/dist/assets/CPInbox-tMSbmQ9H.js +0 -11
  267. package/ui/dist/assets/ChannelsPanel-DP5C2OKd.js +0 -1
  268. package/ui/dist/assets/CheckpointsPanel-DlranVLZ.js +0 -1
  269. package/ui/dist/assets/CommandPostHub-BgxIa4Ev.js +0 -29
  270. package/ui/dist/assets/CronPanel-LoT5yKwJ.js +0 -1
  271. package/ui/dist/assets/DaemonPanel-DBGMqaE_.js +0 -1
  272. package/ui/dist/assets/EvalPanel-Bc33j0pN.js +0 -1
  273. package/ui/dist/assets/FleetPanel-CSsXuQYj.js +0 -1
  274. package/ui/dist/assets/InfraView-CR6HyrL6.js +0 -2
  275. package/ui/dist/assets/InlineEditableField-CnvF-yFR.js +0 -1
  276. package/ui/dist/assets/Input-GTHp2Rkr.js +0 -1
  277. package/ui/dist/assets/IntegrationsPanel-CymCRE3T.js +0 -1
  278. package/ui/dist/assets/IntelligenceView-C1IHxJRC.js +0 -2
  279. package/ui/dist/assets/LearningPanel-DOCES3lH.js +0 -1
  280. package/ui/dist/assets/LogsPanel-BLnAqEaZ.js +0 -1
  281. package/ui/dist/assets/McpPanel-ChUzmr3z.js +0 -1
  282. package/ui/dist/assets/MemoryWikiPanel-Dwk3Aqwd.js +0 -11
  283. package/ui/dist/assets/NvidiaPanel-CeZK_-CV.js +0 -1
  284. package/ui/dist/assets/OrganismPanel-BB6YOiQV.js +0 -1
  285. package/ui/dist/assets/OverviewPanel-BmtBhQnv.js +0 -1
  286. package/ui/dist/assets/PaperclipPanel-C-brgwA3.js +0 -1
  287. package/ui/dist/assets/RecipesPanel-34lCfynJ.js +0 -1
  288. package/ui/dist/assets/SecurityPanel-CBTPWLj6.js +0 -1
  289. package/ui/dist/assets/SelfImprovePanel-BrPbFHhG.js +0 -1
  290. package/ui/dist/assets/SessionsPanel-DAEYIn83.js +0 -1
  291. package/ui/dist/assets/SettingsPanel-CzRROAYQ.js +0 -1
  292. package/ui/dist/assets/SettingsView-CN7ii2uw.js +0 -2
  293. package/ui/dist/assets/SomaView-Ba642Oqb.js +0 -5
  294. package/ui/dist/assets/TeamsPanel-DKQ5z2Qe.js +0 -1
  295. package/ui/dist/assets/TelemetryPanel-B6KAc55Q.js +0 -1
  296. package/ui/dist/assets/TitanCanvas-C-s0A-lv.js +0 -1092
  297. package/ui/dist/assets/ToolsView-Dei0KMP0.js +0 -2
  298. package/ui/dist/assets/TraceViewer-BniolyBx.js +0 -1
  299. package/ui/dist/assets/TrainingPanel-Bz4CTPGW.js +0 -1
  300. package/ui/dist/assets/VoiceOverlay-CmNCrLcd.js +0 -37
  301. package/ui/dist/assets/VramPanel-Xh_OtRDR.js +0 -1
  302. package/ui/dist/assets/WorkTab-BjLNmgIK.js +0 -1
@@ -0,0 +1,770 @@
1
+ #!/usr/bin/env node
2
+ import { Router } from "express";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import fs from "fs";
6
+ import { execSync, spawn } from "child_process";
7
+ import { loadConfig } from "../../config/config.js";
8
+ import logger from "../../utils/logger.js";
9
+ import { routeMessage } from "../../agent/multiAgent.js";
10
+ const COMPONENT = "VoiceRouter";
11
+ const VOICE_POISON_PATTERNS = [
12
+ /i completed the tool operations/i,
13
+ /i wasn't able to execute tools/i,
14
+ /i completed the operations/i,
15
+ /let me know if you need anything else\.?\s*$/i
16
+ ];
17
+ const F5_TTS_DEFAULT_VOICES = ["andrew"];
18
+ const F5_TTS_PORT = 5006;
19
+ const F5_TTS_MODEL = "f5-tts-mlx";
20
+ function createVoiceRouter(sessionAborts, sessionAbortTimes, rateLimit, concurrencyGuard, activeLlmRequestsRef, titanActiveSessions, titanRequestDuration) {
21
+ const router = Router();
22
+ let f5ttsPid = null;
23
+ router.get("/voice/status", async (_req, res) => {
24
+ const cfg2 = loadConfig();
25
+ const voice = cfg2.voice;
26
+ if (!voice.enabled) {
27
+ res.json({ available: false, reason: "Voice not enabled in config" });
28
+ return;
29
+ }
30
+ try {
31
+ const livekitHttp = voice.livekitUrl.replace("ws://", "http://").replace("wss://", "https://");
32
+ const resp = await fetch(livekitHttp, { signal: AbortSignal.timeout(3e3) });
33
+ res.json({ available: resp.ok, livekitUrl: voice.livekitUrl, ttsVoice: voice.ttsVoice });
34
+ } catch {
35
+ res.json({ available: false, livekitUrl: voice.livekitUrl, reason: "LiveKit server unreachable" });
36
+ }
37
+ });
38
+ router.get("/voice/config", (_req, res) => {
39
+ res.json(loadConfig().voice);
40
+ });
41
+ router.post("/livekit/token", async (req, res) => {
42
+ const cfg2 = loadConfig();
43
+ if (!cfg2.voice?.enabled) {
44
+ res.status(404).json({ error: "Voice not enabled" });
45
+ return;
46
+ }
47
+ if (!cfg2.voice.livekitApiKey || !cfg2.voice.livekitApiSecret) {
48
+ res.status(503).json({ error: "LiveKit not configured" });
49
+ return;
50
+ }
51
+ try {
52
+ const livekitSdk = await import("livekit-server-sdk").catch(() => null);
53
+ if (!livekitSdk?.AccessToken) {
54
+ res.status(503).json({ error: "livekit-server-sdk not installed" });
55
+ return;
56
+ }
57
+ const { AccessToken } = livekitSdk;
58
+ const participantIdentity = `voice_user_${Math.floor(Math.random() * 1e4)}`;
59
+ const roomName = `voice_room_${Math.floor(Math.random() * 1e4)}`;
60
+ const at = new AccessToken(cfg2.voice.livekitApiKey, cfg2.voice.livekitApiSecret, { identity: participantIdentity, name: "user", ttl: "15m" });
61
+ at.addGrant({ room: roomName, roomJoin: true, canPublish: true, canPublishData: true, canSubscribe: true });
62
+ let serverUrl = cfg2.voice.livekitUrl;
63
+ try {
64
+ const reqHost = req.hostname || req.headers.host?.split(":")[0];
65
+ if (reqHost) {
66
+ const parsed = new URL(serverUrl);
67
+ parsed.hostname = reqHost;
68
+ serverUrl = parsed.toString().replace(/\/$/, "");
69
+ }
70
+ } catch {
71
+ }
72
+ res.json({ serverUrl, roomName, participantName: "user", participantToken: await at.toJwt() });
73
+ } catch (err) {
74
+ logger.error(COMPONENT, `LiveKit token error: ${err.message}`);
75
+ res.status(500).json({ error: "Failed to generate LiveKit token" });
76
+ }
77
+ });
78
+ router.get("/voice/health", async (_req, res) => {
79
+ const cfg2 = loadConfig();
80
+ if (!cfg2.voice?.enabled) {
81
+ res.json({ livekit: false, stt: false, tts: false, agent: false, overall: false, ttsEngine: cfg2.voice?.ttsEngine || "f5-tts" });
82
+ return;
83
+ }
84
+ const engine = cfg2.voice.ttsEngine || "f5-tts";
85
+ const results = { livekit: false, stt: false, tts: false, agent: false, overall: false, ttsEngine: engine };
86
+ const sttUrl = cfg2.voice.sttUrl || "http://localhost:48421";
87
+ const ttsUrl = cfg2.voice.ttsUrl || "http://localhost:5006";
88
+ const nvidia = cfg2.nvidia;
89
+ const asrCfg = nvidia?.asr;
90
+ const sttHealthUrl = (cfg2.voice.sttEngine || "faster-whisper") === "nemotron-asr" ? `${asrCfg?.healthUrl || "http://localhost:9000"}/v1/health/ready` : `${sttUrl}/health`;
91
+ await Promise.allSettled([
92
+ { key: "livekit", url: cfg2.voice.livekitUrl.replace("ws://", "http://").replace("wss://", "https://") },
93
+ { key: "agent", url: cfg2.voice.agentUrl },
94
+ { key: "stt", url: sttHealthUrl }
95
+ ].map(async ({ key, url }) => {
96
+ try {
97
+ const resp = await fetch(url, { signal: AbortSignal.timeout(3e3) });
98
+ results[key] = resp.ok || resp.status < 500;
99
+ } catch {
100
+ results[key] = false;
101
+ }
102
+ }));
103
+ try {
104
+ let resp = await fetch(`${ttsUrl}/health`, { signal: AbortSignal.timeout(3e3) }).catch(() => null);
105
+ if (!resp || resp.status >= 400) {
106
+ const voice = cfg2.voice.ttsVoice || "andrew";
107
+ resp = await fetch(`${ttsUrl}/v1/audio/speech`, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ model: "f5-tts", input: ".", voice, response_format: "pcm" }),
111
+ signal: AbortSignal.timeout(1e4)
112
+ });
113
+ }
114
+ results.tts = resp ? resp.status < 500 : false;
115
+ } catch {
116
+ results.tts = false;
117
+ }
118
+ results.overall = results.tts;
119
+ res.json(results);
120
+ });
121
+ router.get("/nvidia/health/cuopt", async (_req, res) => {
122
+ const cfg2 = loadConfig();
123
+ const nvidia = cfg2.nvidia;
124
+ const cuoptUrl = nvidia?.cuopt?.url || "http://localhost:5000";
125
+ try {
126
+ const resp = await fetch(`${cuoptUrl}/cuopt/health`, { signal: AbortSignal.timeout(5e3) });
127
+ res.json({ healthy: resp.ok, status: resp.status, url: cuoptUrl });
128
+ } catch {
129
+ res.json({ healthy: false, url: cuoptUrl });
130
+ }
131
+ });
132
+ router.get("/nvidia/health/asr", async (_req, res) => {
133
+ const cfg2 = loadConfig();
134
+ const nvidia = cfg2.nvidia;
135
+ const healthUrl = nvidia?.asr?.healthUrl || "http://localhost:9000";
136
+ try {
137
+ const resp = await fetch(`${healthUrl}/v1/health/ready`, { signal: AbortSignal.timeout(5e3) });
138
+ res.json({ healthy: resp.ok, status: resp.status, url: healthUrl });
139
+ } catch {
140
+ res.json({ healthy: false, url: healthUrl });
141
+ }
142
+ });
143
+ router.get("/nvidia/health/nim", async (_req, res) => {
144
+ const cfg2 = loadConfig();
145
+ const nvidia = cfg2.nvidia;
146
+ const apiKey = nvidia?.apiKey || process.env.NVIDIA_API_KEY || "";
147
+ if (!apiKey) {
148
+ res.json({ healthy: false, reason: "No NVIDIA API key configured" });
149
+ return;
150
+ }
151
+ try {
152
+ const resp = await fetch("https://integrate.api.nvidia.com/v1/models", { headers: { Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(8e3) });
153
+ res.json({ healthy: resp.ok, status: resp.status });
154
+ } catch {
155
+ res.json({ healthy: false, reason: "NIM API unreachable" });
156
+ }
157
+ });
158
+ router.post("/voice/preview", async (req, res) => {
159
+ const cfg2 = loadConfig();
160
+ const engine = cfg2.voice?.ttsEngine || "f5-tts";
161
+ const voiceId = req.body?.voice || cfg2.voice?.ttsVoice || "andrew";
162
+ const rawText = req.body?.text || "Hey! I'm TITAN, your AI assistant.";
163
+ const text = rawText.length > 500 ? rawText.slice(0, 497) + "..." : rawText;
164
+ const ttsUrl = cfg2.voice?.ttsUrl || "http://localhost:5006";
165
+ try {
166
+ const ttsRes = await fetch(`${ttsUrl}/v1/audio/speech`, {
167
+ method: "POST",
168
+ headers: { "Content-Type": "application/json" },
169
+ body: JSON.stringify({ model: "f5-tts-mlx", input: text, voice: voiceId, response_format: "wav" }),
170
+ signal: AbortSignal.timeout(6e4)
171
+ });
172
+ if (!ttsRes.ok) {
173
+ res.status(502).json({ error: `TTS service unavailable`, status: ttsRes.status });
174
+ return;
175
+ }
176
+ res.setHeader("Content-Type", "audio/wav");
177
+ res.send(Buffer.from(await ttsRes.arrayBuffer()));
178
+ } catch {
179
+ res.status(502).json({ error: `TTS service unavailable` });
180
+ }
181
+ });
182
+ router.post(
183
+ "/voice/stream",
184
+ rateLimit ? rateLimit(6e4, 30) : (_req, _res, next) => next(),
185
+ concurrencyGuard ? concurrencyGuard(10) : (_req, _res, next) => next(),
186
+ async (req, res) => {
187
+ const { content, sessionId: requestedSessionId, voice: reqVoice } = req.body || {};
188
+ if (!content) {
189
+ res.status(400).json({ error: "content is required" });
190
+ return;
191
+ }
192
+ const cfg2 = loadConfig();
193
+ const ttsUrl = cfg2.voice?.ttsUrl || "http://localhost:5006";
194
+ const ttsEngine = cfg2.voice?.ttsEngine || "f5-tts";
195
+ const voiceId = reqVoice || cfg2.voice?.ttsVoice || "andrew";
196
+ const channel = "voice";
197
+ const userId = "voice-user";
198
+ res.setHeader("Content-Type", "text/event-stream");
199
+ res.setHeader("Cache-Control", "no-cache");
200
+ res.setHeader("Connection", "keep-alive");
201
+ res.setHeader("X-Accel-Buffering", "no");
202
+ res.flushHeaders();
203
+ let clientDisconnected = false;
204
+ res.on("close", () => {
205
+ clientDisconnected = true;
206
+ });
207
+ const safeWrite = (data) => {
208
+ if (!clientDisconnected) {
209
+ try {
210
+ res.write(data);
211
+ } catch {
212
+ clientDisconnected = true;
213
+ }
214
+ }
215
+ };
216
+ const heartbeat = setInterval(() => {
217
+ if (clientDisconnected) {
218
+ clearInterval(heartbeat);
219
+ return;
220
+ }
221
+ safeWrite(": heartbeat\n\n");
222
+ }, 2e3);
223
+ const abortController = new AbortController();
224
+ if (requestedSessionId) {
225
+ sessionAborts.set(requestedSessionId, abortController);
226
+ sessionAbortTimes.set(requestedSessionId, Date.now());
227
+ }
228
+ let effectiveTtsEngine = ttsEngine;
229
+ const effectiveTtsUrl = ttsUrl;
230
+ const effectiveTtsModel = "f5-tts-mlx";
231
+ try {
232
+ const probe = await fetch(`${effectiveTtsUrl}/health`, { signal: AbortSignal.timeout(5e3) });
233
+ if (!probe.ok) effectiveTtsEngine = "unavailable";
234
+ } catch {
235
+ effectiveTtsEngine = "unavailable";
236
+ logger.warn(COMPONENT, `F5-TTS unreachable at ${effectiveTtsUrl}`);
237
+ }
238
+ safeWrite(`event: tts_mode
239
+ data: ${JSON.stringify({ engine: effectiveTtsEngine })}
240
+
241
+ `);
242
+ let tokenBuffer = "";
243
+ let sentenceIndex = 0;
244
+ let firstChunkSent = false;
245
+ let totalTtsChars = 0;
246
+ const FIRST_CHUNK_MIN = 60;
247
+ const MAX_TTS_SENTENCES = 50;
248
+ const MAX_TTS_CHARS = 1e4;
249
+ const ttsQueue = [];
250
+ let ttsRunning = false;
251
+ let ttsResolve = () => {
252
+ };
253
+ const ttsAllDone = new Promise((resolve) => {
254
+ ttsResolve = resolve;
255
+ });
256
+ let ttsFinished = false;
257
+ const processTtsQueue = async () => {
258
+ if (ttsRunning) return;
259
+ ttsRunning = true;
260
+ while (ttsQueue.length > 0) {
261
+ if (clientDisconnected) break;
262
+ const item = ttsQueue.shift();
263
+ await fireTTSInternal(item.sentence, item.index);
264
+ }
265
+ ttsRunning = false;
266
+ if (ttsFinished && ttsQueue.length === 0) ttsResolve();
267
+ };
268
+ const cleanForVoice = (text) => text.replace(/<TOOLCALL>[\s\S]*?(?:<\/TOOLCALL>|$)/g, "").replace(/<TOOLCALL>\[[\s\S]*?\]/g, "").replace(/```[\s\S]*?```/g, "").replace(/`[^`]+`/g, (m) => m.slice(1, -1)).replace(/\*\*(.*?)\*\*/g, "$1").replace(/\*(.*?)\*/g, "$1").replace(/^#+\s+/gm, "").replace(/^\d+\.\s+/gm, "").replace(/^[-*]\s+/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/https?:\/\/\S+/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").replace(/<(?:laugh|chuckle|sigh|cough|sniffle|groan|yawn|gasp|smile)>/gi, "").replace(/(?:Let me |I'll |I will |I'm going to )(?:use|call|check|run|invoke|execute|try)(?: the)? \w[\w_]*(?: tool)?(?:\s+(?:to|for|and)\b[^.!?]*)?[.!?]?\s*/gi, "").replace(/\b(?:Using|Calling|Running|Checking|Invoking|Executing) (?:the )?\w[\w_]*(?: tool)?(?:\s+(?:to|for)\b[^.!?]*)?[.!?]?\s*/gi, "").replace(/(\w)\s*—\s*(\w)/g, "$1, $2").replace(/(\w)\s*–\s*(\w)/g, "$1, $2").replace(/;\s*/g, ". ").replace(/\(([^)]+)\)/g, ", $1,").replace(/([a-z]{4,}),\s*(but|yet|so|however|although)\s+/gi, "$1. $2 ").replace(/\.\s*\./g, ".").replace(/,\s*\./g, ".").replace(/\s{2,}/g, " ").trim();
269
+ const isF5TTS = effectiveTtsEngine === "f5-tts";
270
+ const f5Sentences = [];
271
+ const fireTTSInternal = async (sentence, index) => {
272
+ const clean = cleanForVoice(sentence);
273
+ if (!clean || clean.length < 3) return;
274
+ if (!isF5TTS) safeWrite(`event: sentence
275
+ data: ${JSON.stringify({ text: clean, index })}
276
+
277
+ `);
278
+ if (index >= MAX_TTS_SENTENCES || totalTtsChars >= MAX_TTS_CHARS) return;
279
+ totalTtsChars += clean.length;
280
+ try {
281
+ const ttsRes = await fetch(`${effectiveTtsUrl}/v1/audio/speech`, {
282
+ method: "POST",
283
+ headers: { "Content-Type": "application/json" },
284
+ body: JSON.stringify({ model: effectiveTtsModel, input: clean, voice: voiceId, response_format: "wav" }),
285
+ signal: AbortSignal.timeout(6e4)
286
+ });
287
+ if (ttsRes.ok && !clientDisconnected) {
288
+ const audioBuffer = Buffer.from(await ttsRes.arrayBuffer());
289
+ safeWrite(`event: audio
290
+ data: ${JSON.stringify({ index, audio: audioBuffer.toString("base64"), format: "wav" })}
291
+
292
+ `);
293
+ }
294
+ } catch (e) {
295
+ logger.debug("Gateway", `Voice stream TTS failed for sentence ${index}: ${e.message}`);
296
+ }
297
+ };
298
+ const flushSentence = (text) => {
299
+ const trimmed = text.trim();
300
+ if (trimmed.length < 3) return;
301
+ if (isF5TTS) {
302
+ const clean = cleanForVoice(trimmed);
303
+ if (clean && clean.length >= 3) {
304
+ safeWrite(`event: sentence
305
+ data: ${JSON.stringify({ text: clean, index: sentenceIndex++ })}
306
+
307
+ `);
308
+ f5Sentences.push(clean);
309
+ }
310
+ return;
311
+ }
312
+ const idx = sentenceIndex++;
313
+ ttsQueue.push({ sentence: trimmed, index: idx });
314
+ processTtsQueue();
315
+ };
316
+ const activeRef = activeLlmRequestsRef || { value: 0 };
317
+ activeRef.value++;
318
+ if (titanActiveSessions) titanActiveSessions.inc();
319
+ const startTime = process.hrtime.bigint();
320
+ try {
321
+ const response = await routeMessage(content, channel, userId, {
322
+ streamCallbacks: {
323
+ onToken: (token) => {
324
+ if (clientDisconnected) return;
325
+ tokenBuffer += token;
326
+ if (!firstChunkSent && tokenBuffer.length >= FIRST_CHUNK_MIN) {
327
+ const lastSpace = tokenBuffer.lastIndexOf(" ");
328
+ if (lastSpace > 30) {
329
+ flushSentence(tokenBuffer.slice(0, lastSpace));
330
+ tokenBuffer = tokenBuffer.slice(lastSpace + 1);
331
+ firstChunkSent = true;
332
+ return;
333
+ }
334
+ }
335
+ if (tokenBuffer.includes("\n")) {
336
+ const lines = tokenBuffer.split("\n");
337
+ tokenBuffer = lines.pop() || "";
338
+ for (const line of lines) {
339
+ if (line.trim().length >= 3) {
340
+ flushSentence(line);
341
+ firstChunkSent = true;
342
+ }
343
+ }
344
+ return;
345
+ }
346
+ let match;
347
+ while ((match = tokenBuffer.match(/^(.*?(?<![\d])\b(?:Dr|Mr|Mrs|Ms|vs|etc|e\.g|i\.e))?([.!?])(\s+|$)/s)) !== null) {
348
+ flushSentence(match[1]);
349
+ tokenBuffer = tokenBuffer.slice(match[0].length);
350
+ firstChunkSent = true;
351
+ }
352
+ if (tokenBuffer.length > 80) {
353
+ const colonMatch = tokenBuffer.match(/^(.*?[:;])\s+/s);
354
+ if (colonMatch && colonMatch[1].length > 20) {
355
+ flushSentence(colonMatch[1]);
356
+ tokenBuffer = tokenBuffer.slice(colonMatch[0].length);
357
+ firstChunkSent = true;
358
+ return;
359
+ }
360
+ }
361
+ if (tokenBuffer.length > 200) {
362
+ const commaPos = tokenBuffer.lastIndexOf(", ", 180);
363
+ if (commaPos > 40) {
364
+ flushSentence(tokenBuffer.slice(0, commaPos + 1));
365
+ tokenBuffer = tokenBuffer.slice(commaPos + 2);
366
+ firstChunkSent = true;
367
+ } else {
368
+ const lastSpace = tokenBuffer.lastIndexOf(" ", 180);
369
+ if (lastSpace > 50) {
370
+ flushSentence(tokenBuffer.slice(0, lastSpace));
371
+ tokenBuffer = tokenBuffer.slice(lastSpace + 1);
372
+ firstChunkSent = true;
373
+ }
374
+ }
375
+ }
376
+ },
377
+ onToolCall: (name) => {
378
+ safeWrite(`event: tool
379
+ data: ${JSON.stringify({ name })}
380
+
381
+ `);
382
+ }
383
+ },
384
+ signal: abortController.signal
385
+ });
386
+ if (tokenBuffer.trim()) {
387
+ flushSentence(tokenBuffer);
388
+ tokenBuffer = "";
389
+ }
390
+ if (isF5TTS && f5Sentences.length > 0) {
391
+ const F5_MAX_CHUNK_CHARS = 600;
392
+ const chunks = [];
393
+ let current = "";
394
+ for (const s of f5Sentences) {
395
+ if (current && current.length + s.length + 1 > F5_MAX_CHUNK_CHARS) {
396
+ chunks.push(current);
397
+ current = s;
398
+ } else {
399
+ current += (current ? " " : "") + s;
400
+ }
401
+ }
402
+ if (current) chunks.push(current);
403
+ let audioIdx = 0;
404
+ for (const chunk of chunks) {
405
+ if (clientDisconnected || totalTtsChars >= MAX_TTS_CHARS) break;
406
+ totalTtsChars += chunk.length;
407
+ try {
408
+ const ttsRes = await fetch(`${effectiveTtsUrl}/v1/audio/speech`, {
409
+ method: "POST",
410
+ headers: { "Content-Type": "application/json" },
411
+ body: JSON.stringify({ model: effectiveTtsModel, input: chunk, voice: voiceId, response_format: "wav" }),
412
+ signal: AbortSignal.timeout(12e4)
413
+ });
414
+ if (ttsRes.ok && !clientDisconnected) {
415
+ const audioBuffer = Buffer.from(await ttsRes.arrayBuffer());
416
+ safeWrite(`event: audio
417
+ data: ${JSON.stringify({ index: audioIdx++, audio: audioBuffer.toString("base64"), format: "wav" })}
418
+
419
+ `);
420
+ }
421
+ } catch (e) {
422
+ logger.debug("Gateway", `F5-TTS chunk ${audioIdx} failed: ${e.message}`);
423
+ }
424
+ }
425
+ }
426
+ ttsFinished = true;
427
+ if (!ttsRunning && ttsQueue.length === 0) ttsResolve();
428
+ if (!isF5TTS) await ttsAllDone;
429
+ const responseText = response.content || "";
430
+ if (VOICE_POISON_PATTERNS.some((p) => p.test(responseText)) || response.durationMs > 6e4 && responseText.length < 50) {
431
+ logger.warn(COMPONENT, `[VoicePoisonGuard] Detected canned/stale response \u2014 resetting voice session ${response.sessionId}`);
432
+ try {
433
+ const { closeSession } = await import("../../agent/session.js");
434
+ closeSession(response.sessionId);
435
+ } catch {
436
+ }
437
+ }
438
+ if (!clientDisconnected) {
439
+ safeWrite(`event: done
440
+ data: ${JSON.stringify({ sessionId: response.sessionId, model: response.model, durationMs: response.durationMs, toolsUsed: response.toolsUsed, fullText: response.content })}
441
+
442
+ `);
443
+ try {
444
+ res.end();
445
+ } catch {
446
+ }
447
+ }
448
+ } catch (error) {
449
+ if (!clientDisconnected) {
450
+ safeWrite(`event: done
451
+ data: ${JSON.stringify({ error: error.message })}
452
+
453
+ `);
454
+ try {
455
+ res.end();
456
+ } catch {
457
+ }
458
+ }
459
+ } finally {
460
+ clearInterval(heartbeat);
461
+ activeRef.value--;
462
+ if (titanActiveSessions) titanActiveSessions.dec();
463
+ if (titanRequestDuration) titanRequestDuration.observe(Number(process.hrtime.bigint() - startTime) / 1e9, { channel });
464
+ if (requestedSessionId) sessionAborts.delete(requestedSessionId);
465
+ }
466
+ }
467
+ );
468
+ router.get("/voice/voices", async (_req, res) => {
469
+ const cfg2 = loadConfig();
470
+ const engine = cfg2.voice?.ttsEngine || "f5-tts";
471
+ const ttsUrl = cfg2.voice?.ttsUrl || "http://localhost:5006";
472
+ if (engine === "f5-tts") {
473
+ const voicesDir = join(homedir(), ".titan", "voices");
474
+ try {
475
+ const files = fs.existsSync(voicesDir) ? fs.readdirSync(voicesDir).filter((f) => f.endsWith(".wav")) : [];
476
+ res.json({ voices: files.length ? files.map((f) => f.replace(".wav", "")) : ["default"], engine: "f5-tts" });
477
+ } catch {
478
+ res.json({ voices: ["default"], engine: "f5-tts" });
479
+ }
480
+ return;
481
+ }
482
+ try {
483
+ const ttsRes = await fetch(`${ttsUrl}/v1/audio/voices`, { signal: AbortSignal.timeout(3e3) });
484
+ if (!ttsRes.ok) throw new Error();
485
+ const data = await ttsRes.json();
486
+ res.json({ ...data, engine: "f5-tts" });
487
+ } catch {
488
+ res.json({ voices: F5_TTS_DEFAULT_VOICES, engine: "f5-tts" });
489
+ }
490
+ });
491
+ router.get("/voice/tts", async (req, res) => {
492
+ try {
493
+ const text = (req.query.text || "").slice(0, 2e3);
494
+ const voice = req.query.voice || "andrew";
495
+ const format = (req.query.format || "mp3").toLowerCase();
496
+ if (!text.trim()) {
497
+ res.status(400).json({ error: "text required" });
498
+ return;
499
+ }
500
+ const cfg2 = loadConfig();
501
+ const ttsUrl = cfg2.voice?.ttsUrl || "http://localhost:5006";
502
+ const ttsRes = await fetch(`${ttsUrl}/v1/audio/speech`, {
503
+ method: "POST",
504
+ headers: { "Content-Type": "application/json" },
505
+ body: JSON.stringify({ input: text, voice, response_format: format }),
506
+ signal: AbortSignal.timeout(18e4)
507
+ });
508
+ if (!ttsRes || !ttsRes.ok) {
509
+ res.status(502).json({ error: "tts backends unavailable" });
510
+ return;
511
+ }
512
+ const contentType = ttsRes.headers.get("content-type") || (format === "wav" ? "audio/wav" : "audio/mpeg");
513
+ const buf = Buffer.from(await ttsRes.arrayBuffer());
514
+ res.setHeader("Content-Type", contentType);
515
+ res.setHeader("Content-Length", String(buf.length));
516
+ res.setHeader("Cache-Control", "no-store");
517
+ res.send(buf);
518
+ } catch (e) {
519
+ logger.error(COMPONENT, `Endpoint error: ${e.message}`);
520
+ res.status(500).json({ error: "Something went wrong on our end. Please try again in a moment." });
521
+ }
522
+ });
523
+ router.get("/voice/f5tts/status", async (_req, res) => {
524
+ let running = false;
525
+ try {
526
+ const probe = await fetch(`http://localhost:${F5_TTS_PORT}/health`, { signal: AbortSignal.timeout(3e3) });
527
+ running = probe.ok;
528
+ } catch {
529
+ }
530
+ const voicesDir = join(homedir(), ".titan", "voices");
531
+ let voices = [];
532
+ try {
533
+ if (fs.existsSync(voicesDir)) voices = fs.readdirSync(voicesDir).filter((f) => f.endsWith(".wav")).map((f) => f.replace(".wav", ""));
534
+ } catch {
535
+ }
536
+ res.json({ installed: true, running, voices, port: F5_TTS_PORT, model: F5_TTS_MODEL });
537
+ });
538
+ router.post("/voice/f5tts/install", async (_req, res) => {
539
+ res.setHeader("Content-Type", "text/event-stream");
540
+ res.setHeader("Cache-Control", "no-cache");
541
+ res.flushHeaders();
542
+ const send = (step, status, detail) => {
543
+ res.write(`data: ${JSON.stringify({ step, status, detail })}
544
+
545
+ `);
546
+ };
547
+ const venvPath = join(homedir(), ".titan", "qwen3tts-venv");
548
+ const voicesDir = join(homedir(), ".titan", "voices");
549
+ try {
550
+ send("venv", "running", "Creating Python virtual environment...");
551
+ if (!fs.existsSync(join(venvPath, "bin", "python"))) {
552
+ execSync(`python3 -m venv "${venvPath}"`, { timeout: 6e4 });
553
+ }
554
+ send("venv", "done");
555
+ const pip = join(venvPath, "bin", "pip");
556
+ send("install", "running", "Installing F5-TTS + MLX dependencies (this may take 2-3 minutes)...");
557
+ execSync(`"${pip}" install f5-tts-mlx "mlx-audio[server]" "setuptools<81" numpy`, { timeout: 6e5 });
558
+ send("install", "done");
559
+ if (!fs.existsSync(voicesDir)) fs.mkdirSync(voicesDir, { recursive: true });
560
+ send("start", "running", "Starting voice cloning server on port 5006...");
561
+ const python = join(venvPath, "bin", "python");
562
+ const serverScript = join(__dirname, "..", "..", "scripts", "f5-tts-server.py");
563
+ const scriptPath = fs.existsSync(serverScript) ? serverScript : join(__dirname, "..", "..", "..", "scripts", "f5-tts-server.py");
564
+ const child = spawn(python, [scriptPath, "--host", "127.0.0.1", "--port", String(5006)], {
565
+ detached: true,
566
+ stdio: ["ignore", "pipe", "pipe"],
567
+ env: { ...process.env, PATH: `${join(venvPath, "bin")}:${process.env.PATH}` }
568
+ });
569
+ child.unref();
570
+ f5ttsPid = child.pid ?? null;
571
+ const pidFile = join(homedir(), ".titan", "f5tts.pid");
572
+ if (child.pid) fs.writeFileSync(pidFile, String(child.pid));
573
+ send("model", "running", "Downloading F5-TTS model (~500MB, first time only)...");
574
+ let ready = false;
575
+ for (let i = 0; i < 120; i++) {
576
+ await new Promise((r) => setTimeout(r, 2e3));
577
+ try {
578
+ const probe = await fetch(`http://localhost:${5006}/health`, { signal: AbortSignal.timeout(5e3) });
579
+ if (probe.ok) {
580
+ ready = true;
581
+ break;
582
+ }
583
+ } catch {
584
+ }
585
+ }
586
+ if (ready) {
587
+ send("model", "done");
588
+ send("complete", "done", "Voice cloning server is ready! (F5-TTS)");
589
+ } else {
590
+ send("model", "error", "Server started but model loading timed out. It may still be downloading \u2014 try again in a few minutes.");
591
+ }
592
+ } catch (e) {
593
+ send("error", "error", e.message);
594
+ }
595
+ res.end();
596
+ });
597
+ const stopF5TTSHandler = (_req, res) => {
598
+ const candidates = [join(homedir(), ".titan", "f5tts.pid"), join(homedir(), ".titan", "qwen3tts.pid")];
599
+ try {
600
+ for (const pidFile of candidates) {
601
+ if (!fs.existsSync(pidFile)) continue;
602
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim());
603
+ try {
604
+ process.kill(pid, "SIGTERM");
605
+ } catch {
606
+ }
607
+ try {
608
+ fs.unlinkSync(pidFile);
609
+ } catch {
610
+ }
611
+ }
612
+ f5ttsPid = null;
613
+ res.json({ ok: true });
614
+ } catch (e) {
615
+ res.json({ ok: false, error: e.message });
616
+ }
617
+ };
618
+ const startF5TTSSidecar = async () => {
619
+ const venvPath = join(homedir(), ".titan", "qwen3tts-venv");
620
+ const python = join(venvPath, "bin", "python");
621
+ const pidFile = join(homedir(), ".titan", "f5tts.pid");
622
+ if (!fs.existsSync(python)) {
623
+ logger.warn(COMPONENT, "F5-TTS not installed \u2014 skipping auto-start");
624
+ return;
625
+ }
626
+ try {
627
+ const probe = await fetch(`http://localhost:${F5_TTS_PORT}/health`, { signal: AbortSignal.timeout(3e3) });
628
+ if (probe.ok) {
629
+ logger.info(COMPONENT, "F5-TTS sidecar already running");
630
+ return;
631
+ }
632
+ } catch {
633
+ }
634
+ try {
635
+ const serverScript = join(__dirname, "..", "..", "scripts", "f5-tts-server.py");
636
+ const scriptPath = fs.existsSync(serverScript) ? serverScript : join(__dirname, "..", "..", "..", "scripts", "f5-tts-server.py");
637
+ const child = spawn(python, [scriptPath, "--host", "127.0.0.1", "--port", String(F5_TTS_PORT)], {
638
+ detached: true,
639
+ stdio: ["ignore", "pipe", "pipe"],
640
+ env: { ...process.env, PATH: `${join(venvPath, "bin")}:${process.env.PATH}` }
641
+ });
642
+ child.unref();
643
+ f5ttsPid = child.pid ?? null;
644
+ if (child.pid) fs.writeFileSync(pidFile, String(child.pid));
645
+ logger.info(COMPONENT, "F5-TTS sidecar auto-starting...");
646
+ } catch (e) {
647
+ logger.error(COMPONENT, `F5-TTS auto-start failed: ${e.message}`);
648
+ }
649
+ };
650
+ const startF5TTSHandler = async (_req, res) => {
651
+ const venvPath = join(homedir(), ".titan", "qwen3tts-venv");
652
+ const python = join(venvPath, "bin", "python");
653
+ const pidFile = join(homedir(), ".titan", "f5tts.pid");
654
+ if (!fs.existsSync(python)) {
655
+ res.status(400).json({ ok: false, error: "F5-TTS not installed. Use POST /api/voice/f5tts/install first." });
656
+ return;
657
+ }
658
+ try {
659
+ const probe = await fetch(`http://localhost:${5006}/health`, { signal: AbortSignal.timeout(3e3) });
660
+ if (probe.ok) {
661
+ res.json({ ok: true, message: "F5-TTS is already running" });
662
+ return;
663
+ }
664
+ } catch {
665
+ }
666
+ try {
667
+ const serverScript = join(__dirname, "..", "..", "scripts", "f5-tts-server.py");
668
+ const scriptPath = fs.existsSync(serverScript) ? serverScript : join(__dirname, "..", "..", "..", "scripts", "f5-tts-server.py");
669
+ const child = spawn(python, [scriptPath, "--host", "127.0.0.1", "--port", String(5006)], {
670
+ detached: true,
671
+ stdio: ["ignore", "pipe", "pipe"],
672
+ env: { ...process.env, PATH: `${join(venvPath, "bin")}:${process.env.PATH}` }
673
+ });
674
+ child.unref();
675
+ f5ttsPid = child.pid ?? null;
676
+ if (child.pid) fs.writeFileSync(pidFile, String(child.pid));
677
+ res.json({ ok: true, message: "F5-TTS server starting \u2014 model loading may take a minute." });
678
+ } catch (e) {
679
+ res.status(500).json({ ok: false, error: e.message });
680
+ }
681
+ };
682
+ router.post("/voice/f5tts/stop", stopF5TTSHandler);
683
+ router.post("/voice/f5tts/start", startF5TTSHandler);
684
+ const deprecationWarn = (alias, canonical) => {
685
+ logger.warn(COMPONENT, `Deprecated route ${alias} called; please switch to ${canonical}.`);
686
+ };
687
+ router.post("/voice/qwen3tts/stop", (req, res) => {
688
+ deprecationWarn("/api/voice/qwen3tts/stop", "/api/voice/f5tts/stop");
689
+ return stopF5TTSHandler(req, res);
690
+ });
691
+ router.post("/voice/qwen3tts/start", (req, res) => {
692
+ deprecationWarn("/api/voice/qwen3tts/start", "/api/voice/f5tts/start");
693
+ return startF5TTSHandler(req, res);
694
+ });
695
+ router.post("/voice/clone/upload", async (req, res) => {
696
+ try {
697
+ const voicesDir = join(homedir(), ".titan", "voices");
698
+ if (!fs.existsSync(voicesDir)) fs.mkdirSync(voicesDir, { recursive: true });
699
+ const voiceName = req.query.name || req.headers["x-voice-name"] || "custom";
700
+ const safeName = voiceName.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 50) || "custom";
701
+ const transcript = req.query.transcript || req.headers["x-voice-transcript"] || "";
702
+ const contentType = req.headers["content-type"] || "";
703
+ if (contentType.includes("application/json")) {
704
+ const body = req.body;
705
+ if (!body.audio) {
706
+ res.status(400).json({ error: "audio (base64) is required" });
707
+ return;
708
+ }
709
+ const audioBuffer = Buffer.from(body.audio, "base64");
710
+ const name = body.name?.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 50) || safeName;
711
+ fs.writeFileSync(join(voicesDir, `${name}.wav`), audioBuffer);
712
+ if (body.transcript || transcript) fs.writeFileSync(join(voicesDir, `${name}.txt`), body.transcript || transcript);
713
+ res.json({ ok: true, voice: name, path: join(voicesDir, `${name}.wav`) });
714
+ } else {
715
+ const chunks = [];
716
+ req.on("data", (chunk) => chunks.push(chunk));
717
+ req.on("end", () => {
718
+ const audioBuffer = Buffer.concat(chunks);
719
+ fs.writeFileSync(join(voicesDir, `${safeName}.wav`), audioBuffer);
720
+ if (transcript) fs.writeFileSync(join(voicesDir, `${safeName}.txt`), transcript);
721
+ res.json({ ok: true, voice: safeName, path: join(voicesDir, `${safeName}.wav`) });
722
+ });
723
+ }
724
+ } catch (e) {
725
+ logger.error(COMPONENT, `Endpoint error: ${e.message}`);
726
+ res.status(500).json({ error: "Something went wrong on our end. Please try again in a moment." });
727
+ }
728
+ });
729
+ router.get("/voice/clone/voices", (_req, res) => {
730
+ const voicesDir = join(homedir(), ".titan", "voices");
731
+ try {
732
+ if (!fs.existsSync(voicesDir)) {
733
+ res.json({ voices: [] });
734
+ return;
735
+ }
736
+ const voices = fs.readdirSync(voicesDir).filter((f) => f.endsWith(".wav")).map((f) => {
737
+ const name = f.replace(".wav", "");
738
+ const hasTranscript = fs.existsSync(join(voicesDir, `${name}.txt`));
739
+ const stat = fs.statSync(join(voicesDir, f));
740
+ return { name, hasTranscript, sizeBytes: stat.size };
741
+ });
742
+ res.json({ voices });
743
+ } catch (e) {
744
+ res.json({ voices: [], error: e.message });
745
+ }
746
+ });
747
+ router.delete("/voice/clone/:name", (req, res) => {
748
+ const voicesDir = join(homedir(), ".titan", "voices");
749
+ const name = req.params.name.replace(/[^a-zA-Z0-9_-]/g, "");
750
+ try {
751
+ const wavPath = join(voicesDir, `${name}.wav`);
752
+ const txtPath = join(voicesDir, `${name}.txt`);
753
+ if (fs.existsSync(wavPath)) fs.unlinkSync(wavPath);
754
+ if (fs.existsSync(txtPath)) fs.unlinkSync(txtPath);
755
+ res.json({ ok: true });
756
+ } catch (e) {
757
+ res.json({ ok: false, error: e.message });
758
+ }
759
+ });
760
+ const cfg = loadConfig();
761
+ if (cfg.voice?.enabled) {
762
+ startF5TTSSidecar().catch(() => {
763
+ });
764
+ }
765
+ return router;
766
+ }
767
+ export {
768
+ createVoiceRouter
769
+ };
770
+ //# sourceMappingURL=voiceRouter.js.map