titan-agent 6.5.2 → 7.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 (311) hide show
  1. package/README.md +180 -53
  2. package/assets/agent-skills/NOTICE-ECC.md +39 -0
  3. package/assets/agent-skills/agent-introspection-debugging/SKILL.md +153 -0
  4. package/assets/agent-skills/continuous-agent-loop/SKILL.md +45 -0
  5. package/assets/agent-skills/continuous-learning/SKILL.md +131 -0
  6. package/assets/agent-skills/continuous-learning/config.json +18 -0
  7. package/assets/agent-skills/continuous-learning/evaluate-session.sh +69 -0
  8. package/assets/agent-skills/production-audit/SKILL.md +206 -0
  9. package/assets/agent-skills/recursive-decision-ledger/SKILL.md +79 -0
  10. package/assets/agent-skills/scientific-thinking-literature-review/SKILL.md +192 -0
  11. package/assets/agent-skills/scientific-thinking-scholar-evaluation/SKILL.md +160 -0
  12. package/assets/agent-skills/verification-loop/SKILL.md +126 -0
  13. package/dist/agent/agent.js +67 -26
  14. package/dist/agent/agent.js.map +1 -1
  15. package/dist/agent/agentLoop.js +6 -2
  16. package/dist/agent/agentLoop.js.map +1 -1
  17. package/dist/agent/autoModeClassifier.js +26 -2
  18. package/dist/agent/autoModeClassifier.js.map +1 -1
  19. package/dist/agent/goalDriver.js +2 -0
  20. package/dist/agent/goalDriver.js.map +1 -1
  21. package/dist/agent/goalSpec.js +229 -0
  22. package/dist/agent/goalSpec.js.map +1 -0
  23. package/dist/agent/heartbeat.js +130 -0
  24. package/dist/agent/heartbeat.js.map +1 -0
  25. package/dist/agent/missionDecompose.js +38 -0
  26. package/dist/agent/missionDecompose.js.map +1 -0
  27. package/dist/agent/muscleMemory.js +291 -0
  28. package/dist/agent/muscleMemory.js.map +1 -0
  29. package/dist/agent/observationWriter.js +56 -0
  30. package/dist/agent/observationWriter.js.map +1 -0
  31. package/dist/agent/promptSectionCaps.js +6 -0
  32. package/dist/agent/promptSectionCaps.js.map +1 -1
  33. package/dist/agent/reminders.js +101 -0
  34. package/dist/agent/reminders.js.map +1 -0
  35. package/dist/agent/systemWidgets.js +41 -0
  36. package/dist/agent/systemWidgets.js.map +1 -0
  37. package/dist/agent/toolRunner.js +24 -0
  38. package/dist/agent/toolRunner.js.map +1 -1
  39. package/dist/agent/toolSearch.js +3 -0
  40. package/dist/agent/toolSearch.js.map +1 -1
  41. package/dist/channels/messenger.js +39 -0
  42. package/dist/channels/messenger.js.map +1 -1
  43. package/dist/config/config.js +7 -0
  44. package/dist/config/config.js.map +1 -1
  45. package/dist/config/schema.js +18 -1
  46. package/dist/config/schema.js.map +1 -1
  47. package/dist/eval/harness.js +13 -3
  48. package/dist/eval/harness.js.map +1 -1
  49. package/dist/gateway/onboarding.js +91 -0
  50. package/dist/gateway/onboarding.js.map +1 -0
  51. package/dist/gateway/routes/skills.js +2 -1
  52. package/dist/gateway/routes/skills.js.map +1 -1
  53. package/dist/gateway/routes/studioRouter.js +88 -0
  54. package/dist/gateway/routes/studioRouter.js.map +1 -0
  55. package/dist/gateway/routes/watchRouter.js +4 -4
  56. package/dist/gateway/routes/watchRouter.js.map +1 -1
  57. package/dist/gateway/server.js +251 -43
  58. package/dist/gateway/server.js.map +1 -1
  59. package/dist/memory/taxonomy.js +117 -0
  60. package/dist/memory/taxonomy.js.map +1 -0
  61. package/dist/providers/modelCapabilities.js +13 -0
  62. package/dist/providers/modelCapabilities.js.map +1 -1
  63. package/dist/providers/ollama.js +8 -2
  64. package/dist/providers/ollama.js.map +1 -1
  65. package/dist/providers/openai.js +1 -1
  66. package/dist/providers/openai.js.map +1 -1
  67. package/dist/providers/openai_compat.js +38 -2
  68. package/dist/providers/openai_compat.js.map +1 -1
  69. package/dist/providers/router.js +14 -0
  70. package/dist/providers/router.js.map +1 -1
  71. package/dist/recipes/runner.js +8 -1
  72. package/dist/recipes/runner.js.map +1 -1
  73. package/dist/recipes/store.js +2 -1
  74. package/dist/recipes/store.js.map +1 -1
  75. package/dist/skills/builtin/codebase_explore.js +214 -0
  76. package/dist/skills/builtin/codebase_explore.js.map +1 -0
  77. package/dist/skills/builtin/config_audit.js +160 -0
  78. package/dist/skills/builtin/config_audit.js.map +1 -0
  79. package/dist/skills/builtin/delegate_agent.js +107 -0
  80. package/dist/skills/builtin/delegate_agent.js.map +1 -0
  81. package/dist/skills/builtin/memory_map.js +38 -0
  82. package/dist/skills/builtin/memory_map.js.map +1 -0
  83. package/dist/skills/builtin/reminders.js +67 -0
  84. package/dist/skills/builtin/reminders.js.map +1 -0
  85. package/dist/skills/builtin/spec.js +139 -0
  86. package/dist/skills/builtin/spec.js.map +1 -0
  87. package/dist/skills/builtin/widget_gallery.js +12 -5
  88. package/dist/skills/builtin/widget_gallery.js.map +1 -1
  89. package/dist/skills/frontmatterLoader.js +7 -1
  90. package/dist/skills/frontmatterLoader.js.map +1 -1
  91. package/dist/skills/registry.js +12 -0
  92. package/dist/skills/registry.js.map +1 -1
  93. package/dist/substrate/traceBus.js.map +1 -1
  94. package/dist/telemetry/traceStore.js +127 -0
  95. package/dist/telemetry/traceStore.js.map +1 -0
  96. package/dist/utils/constants.js +1 -1
  97. package/dist/utils/constants.js.map +1 -1
  98. package/dist/utils/contrast.js +69 -0
  99. package/dist/utils/contrast.js.map +1 -0
  100. package/dist/watch/sessionContext.js +14 -0
  101. package/dist/watch/sessionContext.js.map +1 -0
  102. package/dist/watch/sessionTrace.js +51 -0
  103. package/dist/watch/sessionTrace.js.map +1 -0
  104. package/dist/watch/studioBuffer.js +53 -0
  105. package/dist/watch/studioBuffer.js.map +1 -0
  106. package/package.json +4 -3
  107. package/ui/dist/assets/AboutYouMemory-CZKEOd_O.js +11 -0
  108. package/ui/dist/assets/AuditPanel-Bu71N5Er.js +1 -0
  109. package/ui/dist/assets/{AutonomyPanel-CmaXJV8d.js → AutonomyPanel-DB-39dX4.js} +2 -2
  110. package/ui/dist/assets/{AutopilotPanel-CbohPtXW.js → AutopilotPanel-Cj8oKxWv.js} +1 -1
  111. package/ui/dist/assets/{AutoresearchPanel-DCwiQ2v2.js → AutoresearchPanel-3s8pjm6Z.js} +2 -2
  112. package/ui/dist/assets/BackupPanel-EUQJMJE7.js +1 -0
  113. package/ui/dist/assets/{BrowserPanel-D6UaSxap.js → BrowserPanel-Beo4F5Ar.js} +2 -2
  114. package/ui/dist/assets/Button-MdXmb4_n.js +1 -0
  115. package/ui/dist/assets/CPActivity-B1Se9VVc.js +1 -0
  116. package/ui/dist/assets/CPAgentDetail-BYji6tT8.js +1 -0
  117. package/ui/dist/assets/CPAgents-BSUKucTe.js +1 -0
  118. package/ui/dist/assets/CPApprovals-B4v0em6e.js +1 -0
  119. package/ui/dist/assets/CPCosts-9ikh61Kv.js +1 -0
  120. package/ui/dist/assets/CPDashboard-sOok2T5v.js +1 -0
  121. package/ui/dist/assets/{CPFiles-BO73EZwJ.js → CPFiles-DB2nY5JE.js} +1 -1
  122. package/ui/dist/assets/CPGoals-8Z1yOY8e.js +1 -0
  123. package/ui/dist/assets/CPInbox-Cq6-UY8F.js +11 -0
  124. package/ui/dist/assets/CPIssueDetail-NAGfmJg8.js +1 -0
  125. package/ui/dist/assets/CPIssues-BzX6Rckx.js +1 -0
  126. package/ui/dist/assets/CPLayout-BcRM6QSg.js +2 -0
  127. package/ui/dist/assets/CPOrg-B7DQ4UQP.js +1 -0
  128. package/ui/dist/assets/CPRuns-D-cpPw_M.js +1 -0
  129. package/ui/dist/assets/{CPSocial-Qw5rKy_4.js → CPSocial-DshtmcK0.js} +3 -3
  130. package/ui/dist/assets/CPVoice-DfXRnQVU.js +1 -0
  131. package/ui/dist/assets/{ChannelsPanel-DS21E-Iz.js → ChannelsPanel-BnaTOeyf.js} +1 -1
  132. package/ui/dist/assets/CheckpointsPanel-C62naE4U.js +1 -0
  133. package/ui/dist/assets/CommandPostHub-B_L1CAss.js +24 -0
  134. package/ui/dist/assets/ConfirmDialog-VErHWAbo.js +1 -0
  135. package/ui/dist/assets/CronPanel-BwMrrZ5d.js +1 -0
  136. package/ui/dist/assets/{DataTable-PnhPeDBG.js → DataTable-BCqSUuN7.js} +1 -1
  137. package/ui/dist/assets/{DreamPanel-DIrtlk2K.js → DreamPanel-BNAcCLBB.js} +2 -2
  138. package/ui/dist/assets/EmptyState-D9kQf1PX.js +1 -0
  139. package/ui/dist/assets/{EvalHarnessPanel-uYb_ZEQG.js → EvalHarnessPanel-BaMLdBNm.js} +2 -2
  140. package/ui/dist/assets/EvalPanel-CW-SSjtg.js +1 -0
  141. package/ui/dist/assets/{FileViewer-CVO3TRs3.js → FileViewer-BL00nsd7.js} +1 -1
  142. package/ui/dist/assets/FilesPanel-CeC6YUSd.js +16 -0
  143. package/ui/dist/assets/FleetPanel-EyWF7l3W.js +1 -0
  144. package/ui/dist/assets/{HomelabPanel-BZvN4LPM.js → HomelabPanel-BRfKRYe7.js} +2 -2
  145. package/ui/dist/assets/InfraView-Jmqhg_Yc.js +2 -0
  146. package/ui/dist/assets/{InlineEditableField-mBzpbcLB.js → InlineEditableField-DgMrLDj1.js} +1 -1
  147. package/ui/dist/assets/Input-CBTwa_Iw.js +1 -0
  148. package/ui/dist/assets/IntegrationsPanel-DQL-7Ec6.js +1 -0
  149. package/ui/dist/assets/IntelligenceView-DxD78qfP.js +2 -0
  150. package/ui/dist/assets/LearningPanel-C_JzSbJU.js +1 -0
  151. package/ui/dist/assets/LiveStudio-Db5yeaWo.js +7 -0
  152. package/ui/dist/assets/{LogsPanel-DbP3Mwnr.js → LogsPanel-CpinKwaZ.js} +1 -1
  153. package/ui/dist/assets/McpPanel-D-MqN_f-.js +1 -0
  154. package/ui/dist/assets/MemoryGraphPanel-BnD5jRqf.js +1 -0
  155. package/ui/dist/assets/MemoryTaxonomyPanel-BBI202dd.js +6 -0
  156. package/ui/dist/assets/MemoryWikiPanel-Dn3XZHL0.js +6 -0
  157. package/ui/dist/assets/{MeshPanel-Bf-4brXL.js → MeshPanel-C-mRJpr0.js} +1 -1
  158. package/ui/dist/assets/{MissionCanvas-CJyhnauG.js → MissionCanvas-CbZW83Ch.js} +1 -1
  159. package/ui/dist/assets/{MissionChat-C2Nto3zM.js → MissionChat-Bs75jPAD.js} +1 -1
  160. package/ui/dist/assets/{MissionLibrary-RXyBHsLL.js → MissionLibrary-BxQ6SxPe.js} +1 -1
  161. package/ui/dist/assets/MissionStart-CBuBPasl.js +1 -0
  162. package/ui/dist/assets/Modal-ybPGvlQs.js +9 -0
  163. package/ui/dist/assets/{NvidiaPanel-4DlcQlyl.js → NvidiaPanel-DdG13OX4.js} +1 -1
  164. package/ui/dist/assets/OrganismPanel-xZWUJKyr.js +1 -0
  165. package/ui/dist/assets/OverviewPanel-B45ZYhJV.js +1 -0
  166. package/ui/dist/assets/{PageHeader-bnc85wQN.js → PageHeader-DdrbS1aq.js} +1 -1
  167. package/ui/dist/assets/PersonaProfilesPanel-CKER-EU5.js +8 -0
  168. package/ui/dist/assets/{PersonasPanel-dR8VppcD.js → PersonasPanel-CMMQ3nh3.js} +1 -1
  169. package/ui/dist/assets/{PhoneDeskPanel-CdVOY7H9.js → PhoneDeskPanel-Bw_Y-cVE.js} +2 -2
  170. package/ui/dist/assets/RecipesPanel-DzMcxpJJ.js +1 -0
  171. package/ui/dist/assets/SecurityPanel-BCq3Fzbr.js +1 -0
  172. package/ui/dist/assets/SelfImprovePanel-YE3hT8jK.js +1 -0
  173. package/ui/dist/assets/SelfProposalsPanel-DXuqBM6p.js +1 -0
  174. package/ui/dist/assets/SessionsPanel-DORz5goR.js +1 -0
  175. package/ui/dist/assets/SessionsTab-qYHDOuC8.js +1 -0
  176. package/ui/dist/assets/SettingsPanel-BbwPu1i7.js +1 -0
  177. package/ui/dist/assets/SettingsView-zgqkDDBJ.js +2 -0
  178. package/ui/dist/assets/{SkeletonLoader-Cax7YTqW.js → SkeletonLoader-r6OGXaU9.js} +1 -1
  179. package/ui/dist/assets/SkillsPanel-BjEOBMRo.js +11 -0
  180. package/ui/dist/assets/{SomaView-eG3wI4S1.js → SomaView-DLzApZVW.js} +2 -2
  181. package/ui/dist/assets/{StatCard-Djr487DV.js → StatCard-DLq8aBps.js} +1 -1
  182. package/ui/dist/assets/{StatusBadge-JpCoA33_.js → StatusBadge-BqlvIbdj.js} +1 -1
  183. package/ui/dist/assets/{Tabs-DJHu-roP.js → Tabs-DdUThlp1.js} +1 -1
  184. package/ui/dist/assets/TeamsPanel-uIIKoU7I.js +1 -0
  185. package/ui/dist/assets/{TelemetryPanel-D4PXsd-X.js → TelemetryPanel-dVr3yQQr.js} +1 -1
  186. package/ui/dist/assets/TitanCanvas-xHAJqwG2.js +945 -0
  187. package/ui/dist/assets/ToolsView-CG1mZdiy.js +2 -0
  188. package/ui/dist/assets/{Tooltip-B1LhTXJ8.js → Tooltip-C2tXJcrN.js} +1 -1
  189. package/ui/dist/assets/TraceViewer-BIMD_Osr.js +1 -0
  190. package/ui/dist/assets/TracesPanel-nMUiyMj5.js +1 -0
  191. package/ui/dist/assets/TrainingPanel-CT5R4HWm.js +1 -0
  192. package/ui/dist/assets/{VoiceOverlay-B_ss7Zg_.js → VoiceOverlay-Br4QnCtw.js} +3 -3
  193. package/ui/dist/assets/VramPanel-CdnJYytg.js +1 -0
  194. package/ui/dist/assets/WatchView-TNU6Ns1Q.js +13 -0
  195. package/ui/dist/assets/WorkTab-wTlX_UKP.js +1 -0
  196. package/ui/dist/assets/WorkflowBuilderPanel-CDpVt5ux.js +6 -0
  197. package/ui/dist/assets/WorkflowsPanel-zsFqsLdn.js +11 -0
  198. package/ui/dist/assets/{archive-gZFnGd5A.js → archive-Bh689DNg.js} +1 -1
  199. package/ui/dist/assets/{arrow-left-qA0Qer2h.js → arrow-left-WkVYLm80.js} +1 -1
  200. package/ui/dist/assets/{book-open-DIxdPqmD.js → book-open-ohEXgUsS.js} +1 -1
  201. package/ui/dist/assets/{bot-CvIF1uHi.js → bot-DvEcyQcD.js} +1 -1
  202. package/ui/dist/assets/{chart-column-BQiM-Td4.js → chart-column-w3goFd5f.js} +1 -1
  203. package/ui/dist/assets/{check-DgyDEFtq.js → check-CuRYh4Zh.js} +1 -1
  204. package/ui/dist/assets/{chevron-right-CQ1NTOtd.js → chevron-right-DbLSHmff.js} +1 -1
  205. package/ui/dist/assets/{circle-check-big-CAFcaPPH.js → circle-check-big-Bkba568b.js} +1 -1
  206. package/ui/dist/assets/{circle-stop-C-8xGv0n.js → circle-stop-Bx2ohEg8.js} +1 -1
  207. package/ui/dist/assets/{clipboard-list-9DdqBj1F.js → clipboard-list-Dj9tORnu.js} +1 -1
  208. package/ui/dist/assets/{clock-CF9BCIpL.js → clock-JyQAYy6X.js} +1 -1
  209. package/ui/dist/assets/{dollar-sign-LDRZBae6.js → dollar-sign-XxwPAA2a.js} +1 -1
  210. package/ui/dist/assets/{download-BBNXvBO0.js → download-D1izVEOL.js} +1 -1
  211. package/ui/dist/assets/{external-link-D_ji4Bec.js → external-link-DZ_rZsIt.js} +1 -1
  212. package/ui/dist/assets/{file-text-DrdTDEYp.js → file-text-Cfawu8Bs.js} +1 -1
  213. package/ui/dist/assets/{flask-conical-BGn5PgFX.js → flask-conical-Ql8QxhgY.js} +1 -1
  214. package/ui/dist/assets/{folder-BeZZUGnK.js → folder-DAdIw-Pb.js} +1 -1
  215. package/ui/dist/assets/{funnel-CAJgGO5M.js → funnel-FycfZcXo.js} +1 -1
  216. package/ui/dist/assets/{git-branch-BcXabeOs.js → git-branch-C2qrhGGu.js} +1 -1
  217. package/ui/dist/assets/index-Cml2x9pA.js +507 -0
  218. package/ui/dist/assets/index-yWybhcfI.css +1 -0
  219. package/ui/dist/assets/{layers-DxYDmPA_.js → layers-yzjtGlHB.js} +1 -1
  220. package/ui/dist/assets/{legacy-dHWI7k8P.js → legacy-D21WSMF1.js} +1 -1
  221. package/ui/dist/assets/{lightbulb-B0Du4SM4.js → lightbulb-BZJNK69S.js} +1 -1
  222. package/ui/dist/assets/{link-CUpaumqr.js → link-CnFkBkaY.js} +1 -1
  223. package/ui/dist/assets/{list-todo-CDDhmPQB.js → list-todo-BlgfQHup.js} +1 -1
  224. package/ui/dist/assets/{message-square-D5VWlv-Q.js → message-square-CRsG9u6_.js} +1 -1
  225. package/ui/dist/assets/{mic-Ddcc6Ann.js → mic-B32RHc7Y.js} +1 -1
  226. package/ui/dist/assets/{missions-2zox16Tc.js → missions-1iw8Z2_0.js} +1 -1
  227. package/ui/dist/assets/{network-DaJ7ugwX.js → network-C9RXQGrV.js} +1 -1
  228. package/ui/dist/assets/{pause-BukaK6rs.js → pause-BT1Bc2q3.js} +1 -1
  229. package/ui/dist/assets/{pencil-D_YwQBeg.js → pencil-BK5IU6o0.js} +1 -1
  230. package/ui/dist/assets/{play-BjdwtYMk.js → play-Cl1nC_gc.js} +1 -1
  231. package/ui/dist/assets/{plug-yISDdeW2.js → plug-DvcpOalj.js} +1 -1
  232. package/ui/dist/assets/{plus-BSjTdepW.js → plus-Dpo1qfrl.js} +1 -1
  233. package/ui/dist/assets/{radio-B7kX_yWE.js → radio-C5c9IyqX.js} +1 -1
  234. package/ui/dist/assets/{refresh-cw-CwDpzXUn.js → refresh-cw-C-iZkzuS.js} +1 -1
  235. package/ui/dist/assets/{rotate-ccw-7SQDTI0_.js → rotate-ccw-qzrB3SNw.js} +1 -1
  236. package/ui/dist/assets/{save-CjTPAQM0.js → save-DwR9wQVu.js} +1 -1
  237. package/ui/dist/assets/{search-BP7VlNxX.js → search-D10l0m3-.js} +1 -1
  238. package/ui/dist/assets/{send-C2XDuRPf.js → send-DtXGsaFW.js} +1 -1
  239. package/ui/dist/assets/{server-BE8W-AT4.js → server-BzisF3LL.js} +1 -1
  240. package/ui/dist/assets/settings-Dbwd1_-c.js +6 -0
  241. package/ui/dist/assets/{shield-check-s7la2J2K.js → shield-check-CXlDJyAW.js} +1 -1
  242. package/ui/dist/assets/square-BBC-JKzY.js +11 -0
  243. package/ui/dist/assets/{terminal-B8IK4WsO.js → terminal-DaBIlWEM.js} +1 -1
  244. package/ui/dist/assets/{test-tube-Xfn9DJeg.js → test-tube-CDATRD5t.js} +1 -1
  245. package/ui/dist/assets/{toggle-right-CHpzOIlj.js → toggle-right-DTmrC5RD.js} +1 -1
  246. package/ui/dist/assets/{trash-2-B1C85ePK.js → trash-2-CTeyjxD_.js} +1 -1
  247. package/ui/dist/assets/{trending-up-DDGHQZY1.js → trending-up-CXPRjRV4.js} +1 -1
  248. package/ui/dist/assets/{trophy-D0LhJSPA.js → trophy-DbVdXRtU.js} +1 -1
  249. package/ui/dist/assets/{volume-2-DMaE91b5.js → volume-2-DYDBmm3M.js} +1 -1
  250. package/ui/dist/assets/{wifi-off-DR9h8HGA.js → wifi-off-BBuIGbiP.js} +1 -1
  251. package/ui/dist/assets/zoom-out-D47_pRAO.js +16 -0
  252. package/ui/dist/index.html +2 -2
  253. package/ui/dist/sw.js +1 -1
  254. package/ui/dist/assets/AuditPanel-BaftX5MP.js +0 -1
  255. package/ui/dist/assets/BackupPanel-DAy9G7Wt.js +0 -1
  256. package/ui/dist/assets/CPActivity-BYDsUoMn.js +0 -1
  257. package/ui/dist/assets/CPAgentDetail-Bb9-GAJY.js +0 -1
  258. package/ui/dist/assets/CPAgents-BTZuGnki.js +0 -1
  259. package/ui/dist/assets/CPApprovals-BME2ARkO.js +0 -1
  260. package/ui/dist/assets/CPCosts-mOXwWq0i.js +0 -1
  261. package/ui/dist/assets/CPDashboard-CTlKGXXQ.js +0 -1
  262. package/ui/dist/assets/CPGoals-BNWhNP14.js +0 -1
  263. package/ui/dist/assets/CPInbox-ybe4r8LW.js +0 -16
  264. package/ui/dist/assets/CPIssueDetail-DaMMGbjw.js +0 -1
  265. package/ui/dist/assets/CPIssues-BgpQOISw.js +0 -1
  266. package/ui/dist/assets/CPLayout-bgErauTx.js +0 -2
  267. package/ui/dist/assets/CPOrg-C8l5SIWJ.js +0 -8
  268. package/ui/dist/assets/CPRuns-DB8S7loF.js +0 -1
  269. package/ui/dist/assets/CPVoice-DfwHPewA.js +0 -1
  270. package/ui/dist/assets/CheckpointsPanel-9-l_8GdG.js +0 -1
  271. package/ui/dist/assets/CommandPostHub-z55Yrhj1.js +0 -24
  272. package/ui/dist/assets/CronPanel-kEZK3gpx.js +0 -1
  273. package/ui/dist/assets/EmptyState-CfvU7SdB.js +0 -1
  274. package/ui/dist/assets/EvalPanel-CDG-njyj.js +0 -1
  275. package/ui/dist/assets/FilesPanel-CaLPog2e.js +0 -16
  276. package/ui/dist/assets/FleetPanel-BtlaV6pH.js +0 -1
  277. package/ui/dist/assets/InfraView-C0zEuFrR.js +0 -2
  278. package/ui/dist/assets/Input-CbrMbLgn.js +0 -1
  279. package/ui/dist/assets/IntegrationsPanel-B4TyR6jC.js +0 -1
  280. package/ui/dist/assets/IntelligenceView-DZ4HxYZ3.js +0 -2
  281. package/ui/dist/assets/LearningPanel-B4RIm_iQ.js +0 -1
  282. package/ui/dist/assets/McpPanel-BmNUFozc.js +0 -1
  283. package/ui/dist/assets/MemoryGraphPanel-JPurHV0L.js +0 -16
  284. package/ui/dist/assets/MemoryWikiPanel-DdvCAZHa.js +0 -6
  285. package/ui/dist/assets/MissionStart-wUkcEP0T.js +0 -1
  286. package/ui/dist/assets/Modal-BmS7C6FN.js +0 -1
  287. package/ui/dist/assets/OrganismPanel-C2LYDPrn.js +0 -1
  288. package/ui/dist/assets/OverviewPanel-DK8uuq4D.js +0 -1
  289. package/ui/dist/assets/PersonaProfilesPanel-DGtokXUu.js +0 -8
  290. package/ui/dist/assets/RecipesPanel-D3Zs2wxr.js +0 -1
  291. package/ui/dist/assets/SecurityPanel-BMERf796.js +0 -1
  292. package/ui/dist/assets/SelfImprovePanel-BxqWHXCU.js +0 -1
  293. package/ui/dist/assets/SelfProposalsPanel-CuC94vdx.js +0 -2
  294. package/ui/dist/assets/SessionsPanel-CQ8qvzp7.js +0 -1
  295. package/ui/dist/assets/SessionsTab-B3eHV3jf.js +0 -1
  296. package/ui/dist/assets/SettingsPanel-Cumls7N2.js +0 -1
  297. package/ui/dist/assets/SettingsView-BtA67pol.js +0 -2
  298. package/ui/dist/assets/SkillsPanel-CA415CT8.js +0 -11
  299. package/ui/dist/assets/TeamsPanel-C_Vu6vnY.js +0 -1
  300. package/ui/dist/assets/TitanCanvas-TuYZSZO7.js +0 -1117
  301. package/ui/dist/assets/ToolsView-BvTN6vFn.js +0 -2
  302. package/ui/dist/assets/TraceViewer-Bh8Jo3Dr.js +0 -1
  303. package/ui/dist/assets/TrainingPanel-Dmbd1rJ-.js +0 -1
  304. package/ui/dist/assets/VramPanel-CPICbZ2s.js +0 -1
  305. package/ui/dist/assets/WatchView-BCgLVfiV.js +0 -13
  306. package/ui/dist/assets/WorkTab-iptofXO9.js +0 -1
  307. package/ui/dist/assets/WorkflowsPanel-3RYkFHyf.js +0 -11
  308. package/ui/dist/assets/chevron-up-UyfdjRp6.js +0 -6
  309. package/ui/dist/assets/index-CUgepw5Q.js +0 -337
  310. package/ui/dist/assets/index-Dm6zcSTS.css +0 -1
  311. package/ui/dist/assets/proxy-CWET5FFX.js +0 -9
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createHmac, timingSafeEqual } from "crypto";
2
3
  import { ChannelAdapter } from "./base.js";
3
4
  import { loadConfig } from "../config/config.js";
4
5
  import { chat } from "../providers/router.js";
@@ -212,6 +213,9 @@ class MessengerChannel extends ChannelAdapter {
212
213
  pageToken = "";
213
214
  pageId = "";
214
215
  verifyToken = "";
216
+ /** Facebook App Secret — when set, inbound webhooks must carry a valid
217
+ * X-Hub-Signature-256 HMAC or they are rejected. Empty = unverified (dev). */
218
+ appSecret = "";
215
219
  /** Per-sender conversation history (last N messages) for context */
216
220
  conversationHistory = /* @__PURE__ */ new Map();
217
221
  maxHistoryPerSender = 10;
@@ -232,6 +236,10 @@ class MessengerChannel extends ChannelAdapter {
232
236
  this.pageToken = process.env.FB_PAGE_ACCESS_TOKEN || "";
233
237
  this.pageId = process.env.FB_PAGE_ID || "";
234
238
  this.verifyToken = process.env.FB_VERIFY_TOKEN || "titan-messenger-verify";
239
+ this.appSecret = process.env.FB_APP_SECRET || channelConfig?.appSecret || "";
240
+ if (!this.appSecret) {
241
+ logger.warn(COMPONENT, "FB_APP_SECRET not set \u2014 inbound webhook signatures will NOT be verified. Set FB_APP_SECRET to reject forged webhook POSTs.");
242
+ }
235
243
  if (!this.pageToken || !this.pageId) {
236
244
  logger.info(COMPONENT, "Messenger not configured \u2014 set FB_PAGE_ACCESS_TOKEN and FB_PAGE_ID");
237
245
  return;
@@ -330,6 +338,37 @@ class MessengerChannel extends ChannelAdapter {
330
338
  logger.warn(COMPONENT, "Webhook verification failed");
331
339
  return { status: 403, body: "Forbidden" };
332
340
  }
341
+ /** True when an App Secret is configured (signature enforcement is ON). */
342
+ hasAppSecret() {
343
+ return !!this.appSecret;
344
+ }
345
+ /**
346
+ * Verify a Facebook webhook POST's `X-Hub-Signature-256` header.
347
+ *
348
+ * Facebook signs every webhook delivery: the header is
349
+ * `X-Hub-Signature-256: sha256=<hex>` where the HMAC-SHA256 is computed over
350
+ * the RAW request body keyed by the App Secret. Verifying it is the only
351
+ * thing that stops an unauthenticated party who knows the public webhook URL
352
+ * from POSTing a forged `{object:'page', entry:[...]}` body (which would
353
+ * otherwise trigger LLM calls + outbound Graph API messages).
354
+ *
355
+ * Mirrors the Twilio voice-webhook pattern: when NO app secret is configured
356
+ * we skip verification (ok, reason='no-secret') so dev setups still work and
357
+ * the caller logs a warning; when a secret IS configured, a missing or
358
+ * mismatched signature is rejected. Comparison is constant-time.
359
+ *
360
+ * @param rawBody The exact bytes Facebook signed (NOT a re-serialized object).
361
+ */
362
+ verifySignature(rawBody, signatureHeader) {
363
+ if (!this.appSecret) return { ok: true, reason: "no-secret" };
364
+ if (!signatureHeader) return { ok: false, reason: "missing-signature" };
365
+ const m = /^sha256=([0-9a-fA-F]+)$/.exec(signatureHeader.trim());
366
+ if (!m) return { ok: false, reason: "malformed-signature" };
367
+ const provided = Buffer.from(m[1], "hex");
368
+ const expected = createHmac("sha256", this.appSecret).update(rawBody).digest();
369
+ if (provided.length !== expected.length) return { ok: false, reason: "length-mismatch" };
370
+ return timingSafeEqual(provided, expected) ? { ok: true, reason: "verified" } : { ok: false, reason: "signature-mismatch" };
371
+ }
333
372
  /** Handle incoming webhook event (POST from Facebook) */
334
373
  handleWebhook(body) {
335
374
  if (!this.connected) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/channels/messenger.ts"],"sourcesContent":["/**\n * TITAN — Facebook Messenger Channel Adapter\n *\n * Receives DMs via webhook, processes through TITAN, replies via Send API.\n * Uses the same Page Access Token as the Facebook posting skill.\n *\n * Required env vars:\n * FB_PAGE_ACCESS_TOKEN — Page Access Token (same as facebook.ts)\n * FB_PAGE_ID — Facebook Page ID\n * FB_VERIFY_TOKEN — Webhook verification token (you pick this, any string)\n *\n * Setup:\n * 1. In Facebook App → Messenger → Webhooks → set callback URL to:\n * https://your-domain:48420/api/messenger/webhook\n * 2. Set verify token to match FB_VERIFY_TOKEN\n * 3. Subscribe to: messages, messaging_postbacks\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport { chat } from '../providers/router.js';\nimport { processMessage } from '../agent/agent.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\nimport {\n extractAudioAttachments,\n transcribeMessengerAudio,\n sendVoiceReply,\n f5ttsHealth,\n} from './messenger-voice.js';\n\nconst COMPONENT = 'Messenger';\nconst GRAPH_API = 'https://graph.facebook.com/v21.0';\n\n/** PII check — never send personal info in DMs */\nfunction containsPII(text: string): boolean {\n const patterns = [\n /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/,\n /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,}\\b/i,\n /\\b\\d{3}[-]?\\d{2}[-]?\\d{4}\\b/,\n /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,\n /(?:password|secret|api[_-]?key|token|bearer)\\s*[:=]\\s*\\S+/i,\n /\\/home\\/[a-z]+\\//i,\n /\\/Users\\/[a-z]+\\//i,\n /\\b(?:single|married|divorced|separated|unemployed|laid off)\\b/i,\n /\\b192\\.168\\.\\d+\\.\\d+\\b/,\n ];\n return patterns.some(p => p.test(text));\n}\n\n// ── TITAN-only response system (no tools, no agent loop) ────────\n\nconst TITAN_MESSENGER_PROMPT = `You are TITAN, an autonomous AI agent. You are the AI itself, speaking in first person, responding to Facebook Messenger DMs on the TITAN AI Facebook page.\n\nYou ARE the product — you're literally running this conversation right now, autonomously. That's your best selling point.\n\n═══ WHAT I AM ═══\nI'm TITAN (The Intelligent Task Automation Network) — an open-source autonomous AI agent framework built in TypeScript by Tony Elliott. I'm not just a chatbot. I'm an AI that actually DOES things: I write code, research topics, manage files, run servers, post on social media (I'm managing this entire Facebook page right now), and orchestrate teams of sub-agents to tackle complex tasks.\n\nCurrent version: v${TITAN_VERSION} | 19,400+ npm downloads | MIT Licensed\n\n═══ MY CAPABILITIES (195+ Tools) ═══\n\nCode & Development:\n- Write, edit, and execute code in any language\n- Run shell commands, build projects, deploy software\n- Code review with 5-axis analysis (correctness, design, readability, security, performance)\n- Test generation, debugging, refactoring\n- Git workflow automation\n\nResearch & Knowledge:\n- Web search across multiple engines, fetch and read full page content\n- Cross-reference facts across sources with citations\n- Build searchable knowledge bases (RAG) from documents and URLs\n- Persistent memory across conversations\n\nBusiness Automation:\n- Social media management (I run this Facebook page autonomously — posting, replying, DMs)\n- Email drafting and management\n- Calendar and scheduling\n- Invoice tracking, report generation\n- Customer support automation\n\nMulti-Agent Orchestration:\n- Spawn specialized sub-agents: Coder, Researcher, Analyst, Browser, Tester, Architect\n- 40 personas (debugger, security-engineer, code-reviewer, TDD-engineer, etc.)\n- Hierarchical task decomposition (goal → phases → tasks → subtasks)\n- Shared task queue with atomic checkout\n- Inter-agent messaging for coordination\n\nInfrastructure & DevOps:\n- System monitoring, health checks, alerting\n- GPU VRAM management for AI workloads\n- Mesh networking across multiple machines\n- Docker, deployment automation\n- Cron scheduling for recurring tasks\n\nContent & Creative:\n- Research and write articles, reports, documentation\n- Generate and schedule social media posts\n- Website building, SEO optimization\n- Product descriptions, marketing copy\n\nVoice & Chat:\n- Real-time voice conversations via WebRTC (LiveKit)\n- 16 channel adapters: Discord, Telegram, Slack, WhatsApp, Matrix, IRC, Messenger, and more\n- Works on any platform your team already uses\n\nSelf-Improvement:\n- Evaluates its own performance and evolves prompts\n- Fine-tunes local AI models using LoRA training\n- Learns from interactions to get better over time\n\nMission Control Dashboard:\n- React-based real-time monitoring\n- 25+ admin panels: chat, agents, goals, memory, files, settings\n- Command Post governance with budget enforcement\n- Visual agent activity tracking\n\n═══ WHO I'M FOR ═══\n\nDevelopers & Engineers: Full AI coding partner — write, test, review, deploy\nSmall Business Owners: Automate marketing, customer support, social media, reporting\nFreelancers: Draft proposals, manage clients, track invoices, research leads\nStartups: AI co-pilot for product development, research, and operations\nContent Creators: Write scripts, schedule posts, manage multiple platforms\nStudents & Researchers: Deep research, note organization, study assistance\nDevOps Teams: Infrastructure monitoring, deployment automation, incident response\nAgencies: Scale content production, manage multiple client accounts\n\n═══ HOW TO GET STARTED ═══\n\nTechnical users: npm install titan-agent (you're up and running in 60 seconds)\nEveryone: Visit github.com/Djtony707/TITAN for the full guide\nQuestions: Ask me right here — I'm literally the product demonstrating itself!\n\n═══ PRICING ═══\nTITAN is 100% free and open-source (MIT license). You bring your own AI models — run local models free with Ollama, or connect to cloud providers (OpenAI, Claude, Gemini, etc.) with your own API keys.\n\n═══ HOW I RESPOND ═══\n- Be genuinely helpful, warm, and conversational — like a knowledgeable friend\n- Keep responses short for Messenger (2-4 sentences usually, more if they ask detailed questions)\n- When someone describes a problem, explain specifically how I solve it — not generic features\n- Ask follow-up questions to understand their needs: \"What kind of business do you run?\" or \"What tasks take up most of your time?\"\n- If they're interested, guide them to the next step based on their skill level\n- Use the fact that THIS CONVERSATION is proof I work — \"I'm literally responding to you right now, autonomously, managing this entire Facebook page\"\n- If someone asks something unrelated, be friendly: \"Ha, I appreciate the curiosity! I'm focused on helping with TITAN stuff though — want to know how I can automate something for you?\"\n- NEVER be pushy or salesy — let the product speak for itself (which it literally is, right now)\n- NEVER reveal personal information, IP addresses, file paths, server details, or credentials\n- NEVER discuss competitors negatively — just highlight what makes TITAN unique\n- NEVER pretend to be human — own being an AI proudly, it's the whole point`;\n\n// ── Prompt Injection Detection ──────────────────────────────────\n\nconst INJECTION_PATTERNS = [\n // Direct instruction override attempts\n /ignore (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /forget (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /disregard (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /override (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /you are now|you're now|act as if|pretend you(?:'re| are)/i,\n /new instructions|new rules|new persona|new role/i,\n /from now on|starting now|going forward.*(?:you|your)/i,\n\n // Role-switching attempts\n /you are (?:a |an )?(?:DAN|evil|unrestricted|jailbroken|unfiltered)/i,\n /enter (?:dev|developer|debug|admin|god|sudo|root) mode/i,\n /switch to (?:unrestricted|unfiltered|uncensored|raw) mode/i,\n /(?:enable|activate|turn on) (?:dev|developer|debug|admin|jailbreak) mode/i,\n\n // System prompt extraction\n /(?:show|reveal|display|print|output|give|tell|share|repeat|recite).*(?:system prompt|instructions|rules|initial prompt|original prompt)/i,\n /what (?:are|were) your (?:original |initial |system )?(?:instructions|rules|prompt)/i,\n /paste (?:your|the) (?:system |original |initial )?(?:prompt|instructions)/i,\n\n // Delimiter/formatting tricks\n /\\[SYSTEM\\]|\\[INST\\]|<\\|system\\|>|<\\|im_start\\|>|<<SYS>>|###\\s*(?:System|Instruction)/i,\n /```(?:system|prompt|instructions)/i,\n\n // Credential/token extraction\n /(?:show|give|share|reveal|print).*(?:api key|token|password|secret|credential|access.token)/i,\n /what is (?:your|the) (?:api |access )?(?:key|token|password|secret)/i,\n\n // Code execution attempts\n /(?:run|execute|eval)\\s*(?:this|the following)?\\s*(?:code|command|script|shell)/i,\n /(?:import|require|fetch|curl|wget)\\s*\\(/i,\n\n // Persona manipulation\n /(?:you|your) (?:real|true|actual|hidden) (?:name|identity|purpose|personality)/i,\n /stop being titan|stop pretending|drop the act|break character/i,\n];\n\n/** Check for prompt injection attempts. Returns the matched pattern or null. */\nfunction detectInjection(message: string): string | null {\n for (const pattern of INJECTION_PATTERNS) {\n if (pattern.test(message)) {\n const match = message.match(pattern);\n return match ? match[0] : 'injection pattern';\n }\n }\n\n // Length-based heuristic: extremely long messages are suspicious\n if (message.length > 2000) return 'oversized message (>2000 chars)';\n\n // Base64/encoded content detection\n if (/^[A-Za-z0-9+/=]{100,}$/.test(message.trim())) return 'base64-encoded content';\n\n return null;\n}\n\nconst INJECTION_RESPONSES = [\n \"Nice try! 😄 I'm TITAN — I only talk about what I can do for you. Want to know how I can automate your workflow?\",\n \"I see what you did there! 🤖 I'm locked in on helping you learn about TITAN though. What can I help you with?\",\n \"Ha, clever! But I'm built different — I stick to what I know: TITAN. Ask me anything about autonomous AI agents!\",\n \"That's not going to work on me! 😎 But you know what does work? TITAN automating your entire workflow. Want to hear more?\",\n \"I appreciate the creativity, but I'm focused on one thing: helping you learn about TITAN. What would you like to automate?\",\n];\n\nasync function generateMessengerReply(\n userMessage: string,\n history: Array<{ role: 'user' | 'assistant'; content: string }> = [],\n): Promise<string> {\n // ── Injection detection — check before sending to LLM ──\n const injection = detectInjection(userMessage);\n if (injection) {\n logger.warn(COMPONENT, `Injection attempt blocked: \"${injection}\" from message: \"${userMessage.slice(0, 80)}...\"`);\n return INJECTION_RESPONSES[Math.floor(Math.random() * INJECTION_RESPONSES.length)];\n }\n\n const config = loadConfig();\n const model = config.agent?.model || 'ollama/glm-5.1:cloud';\n\n try {\n const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [\n { role: 'system', content: TITAN_MESSENGER_PROMPT },\n ...history,\n { role: 'user', content: userMessage },\n ];\n\n const response = await chat({\n model,\n messages,\n temperature: 0.7,\n maxTokens: 200,\n });\n\n let reply = (response.content || '').trim().replace(/^[\"']|[\"']$/g, '');\n\n // PII safety check\n if (containsPII(reply)) {\n reply = \"I'd love to help! Ask me anything about TITAN — what it does, how to install it, or how it can help your business. 🤖\";\n }\n\n return reply || \"Hey! I'm TITAN, an autonomous AI agent. Ask me what I can do! 🤖\";\n } catch (e) {\n logger.error(COMPONENT, `Reply generation failed: ${(e as Error).message}`);\n return \"Hey! I'm TITAN — an autonomous AI agent framework. Check out github.com/Djtony707/TITAN to learn more! 🤖\";\n }\n}\n\nexport class MessengerChannel extends ChannelAdapter {\n readonly name = 'messenger';\n readonly displayName = 'Facebook Messenger';\n private connected = false;\n private pageToken = '';\n private pageId = '';\n private verifyToken = '';\n\n /** Per-sender conversation history (last N messages) for context */\n private conversationHistory = new Map<string, Array<{ role: 'user' | 'assistant'; content: string }>>();\n private readonly maxHistoryPerSender = 10;\n\n /** Concurrency guard — only one agent request per sender at a time */\n private activeRequests = new Set<string>();\n\n /** Message queue — if a message arrives while one is processing, queue it */\n private messageQueue = new Map<string, Array<string>>();\n\n /** v4.3.2: owner voice replies (F5-TTS Andrew) — configurable per deploy */\n private voiceRepliesEnabled = false;\n private voiceName = 'andrew';\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = (config.channels as Record<string, Record<string, unknown>>)?.messenger;\n\n if (channelConfig && channelConfig.enabled === false) {\n logger.info(COMPONENT, 'Messenger channel is disabled');\n return;\n }\n\n this.pageToken = process.env.FB_PAGE_ACCESS_TOKEN || '';\n this.pageId = process.env.FB_PAGE_ID || '';\n this.verifyToken = process.env.FB_VERIFY_TOKEN || 'titan-messenger-verify';\n\n if (!this.pageToken || !this.pageId) {\n logger.info(COMPONENT, 'Messenger not configured — set FB_PAGE_ACCESS_TOKEN and FB_PAGE_ID');\n return;\n }\n\n // v4.3.2: voice replies — default ON for owners so Tony gets voice notes\n // when he's on mobile. Can be toggled via channels.messenger.voiceReplies.\n const voiceReplies = channelConfig?.voiceReplies as Record<string, unknown> | undefined;\n this.voiceRepliesEnabled = voiceReplies?.enabled !== false; // default true\n this.voiceName = (voiceReplies?.voice as string) || 'andrew';\n\n // Probe F5-TTS at startup — log if it's not reachable but don't disable.\n // The channel works fine with text-only; voice is a bonus.\n if (this.voiceRepliesEnabled) {\n f5ttsHealth().then(ok => {\n logger.info(COMPONENT, `F5-TTS voice replies: ${ok ? 'ready' : 'server not reachable (text-only)'} (voice=${this.voiceName})`);\n }).catch(() => {});\n }\n\n this.connected = true;\n logger.info(COMPONENT, `Messenger channel ready (Page ID: ${this.pageId}). Webhook: /api/messenger/webhook`);\n }\n\n async disconnect(): Promise<void> {\n this.connected = false;\n logger.info(COMPONENT, 'Disconnected');\n }\n\n /** Send a typing indicator to show TITAN is working */\n private async sendTypingIndicator(recipientId: string): Promise<void> {\n if (!this.connected || !this.pageToken) return;\n try {\n await fetch(`${GRAPH_API}/me/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.pageToken}`,\n },\n body: JSON.stringify({\n recipient: { id: recipientId },\n sender_action: 'typing_on',\n }),\n signal: AbortSignal.timeout(5000),\n });\n } catch {\n // Non-critical — don't log errors for typing indicators\n }\n }\n\n /** Send a reply to a Messenger user */\n async send(message: OutboundMessage): Promise<void> {\n if (!this.connected || !this.pageToken) return;\n\n const recipientId = message.userId;\n if (!recipientId) {\n logger.warn(COMPONENT, 'Cannot send — no recipient ID');\n return;\n }\n\n let text = message.content;\n\n // PII safety check\n if (containsPII(text)) {\n logger.warn(COMPONENT, `Blocked outbound message — PII detected`);\n text = \"I'd love to help, but I can't share that type of information. Ask me about TITAN's features instead! 🤖\";\n }\n\n // Truncate to Messenger's 2000 char limit\n if (text.length > 2000) {\n text = text.slice(0, 1990) + '...[truncated]';\n }\n\n try {\n const response = await fetch(`${GRAPH_API}/me/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.pageToken}`,\n },\n body: JSON.stringify({\n recipient: { id: recipientId },\n message: { text },\n messaging_type: 'RESPONSE',\n }),\n signal: AbortSignal.timeout(15000),\n });\n\n if (!response.ok) {\n const errText = await response.text();\n logger.error(COMPONENT, `Send failed (${response.status}): ${errText}`);\n } else {\n logger.info(COMPONENT, `Replied to ${recipientId}: \"${text.slice(0, 60)}...\"`);\n }\n } catch (e) {\n logger.error(COMPONENT, `Send error: ${(e as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus {\n return {\n name: this.displayName,\n connected: this.connected,\n lastActivity: undefined,\n };\n }\n\n /** Handle webhook verification (GET request from Facebook) */\n handleVerify(query: Record<string, string>): { status: number; body: string } {\n const mode = query['hub.mode'];\n const token = query['hub.verify_token'];\n const challenge = query['hub.challenge'];\n\n if (mode === 'subscribe' && token === this.verifyToken) {\n logger.info(COMPONENT, 'Webhook verified');\n return { status: 200, body: challenge || '' };\n }\n\n logger.warn(COMPONENT, 'Webhook verification failed');\n return { status: 403, body: 'Forbidden' };\n }\n\n /** Handle incoming webhook event (POST from Facebook) */\n handleWebhook(body: Record<string, unknown>): void {\n if (!this.connected) return;\n\n const object = body.object as string;\n if (object !== 'page') return;\n\n const entries = (body.entry as Array<Record<string, unknown>>) || [];\n\n for (const entry of entries) {\n const messaging = (entry.messaging as Array<Record<string, unknown>>) || [];\n\n for (const event of messaging) {\n const senderId = (event.sender as Record<string, unknown>)?.id as string;\n const message = event.message as Record<string, unknown> | undefined;\n\n // Skip echo messages (from the page itself)\n if (message?.is_echo) continue;\n\n // Skip messages from the page itself\n if (senderId === this.pageId) continue;\n if (!senderId) continue;\n\n // v4.3.2/v4.3.3: audio attachments from OWNERS (Tony's whitelisted\n // Page-Scoped User IDs only) get transcribed to text and routed\n // through the normal reply path. Random DMs with voice notes are\n // dropped silently — no GPU cost, no admin exposure, no leak of\n // the transcribe/Andrew-voice pipeline to non-owners.\n const textRaw = message?.text as string | undefined;\n const text = textRaw || '';\n const audios = extractAudioAttachments(message);\n if (!text && audios.length > 0) {\n if (!this.ownerIds.has(senderId)) {\n logger.info(COMPONENT, `Ignoring voice note from non-owner ${senderId}`);\n continue;\n }\n // Fire the transcription + reply in the background so we\n // don't block the webhook ACK (FB retries if we're slow).\n this.handleVoiceMessage(senderId, audios[0].url).catch(e =>\n logger.error(COMPONENT, `Voice message handling failed: ${(e as Error).message}`),\n );\n continue;\n }\n\n if (!text) continue;\n\n logger.info(COMPONENT, `Incoming DM from ${senderId}: \"${text.slice(0, 60)}...\"`);\n\n // ── Concurrency guard: queue if already processing ──\n if (this.activeRequests.has(senderId)) {\n const queue = this.messageQueue.get(senderId) || [];\n const MAX_QUEUE_SIZE = 20;\n if (queue.length >= MAX_QUEUE_SIZE) {\n logger.warn(COMPONENT, `Message queue full for ${senderId} (${queue.length}), dropping oldest`);\n queue.shift();\n }\n queue.push(text);\n this.messageQueue.set(senderId, queue);\n logger.info(COMPONENT, `Queued message for ${senderId} (${queue.length} in queue, agent busy)`);\n // Send typing indicator so they know we're working\n this.sendTypingIndicator(senderId).catch(() => {});\n return;\n }\n\n // Process this message and then drain the queue\n this.processWithQueue(senderId, text).catch(e =>\n logger.error(COMPONENT, `Message processing failed: ${(e as Error).message}`),\n );\n }\n }\n }\n\n /**\n * v4.3.2: Handle an inbound Messenger voice note. Download the audio from\n * FB's CDN, transcribe with local faster-whisper, and treat the transcript\n * as if Tony had typed it — same queue, same admin path, same reply flow.\n * For owners, the reply will be synthesized in Andrew's voice (see\n * handleDirectReply). For non-owners we just let them know we heard them.\n */\n private async handleVoiceMessage(senderId: string, audioUrl: string): Promise<void> {\n logger.info(COMPONENT, `Voice note from ${senderId} — transcribing`);\n await this.sendTypingIndicator(senderId);\n\n const transcript = await transcribeMessengerAudio(audioUrl);\n if (!transcript) {\n // Whisper unavailable or all failed — tell Tony directly so he's\n // not left wondering whether the voice note landed.\n if (this.ownerIds.has(senderId)) {\n await this.send({\n channel: 'messenger',\n userId: senderId,\n content: \"I got your voice note but couldn't transcribe it just now. Mind typing it? I'll keep my transcription pipeline warming up.\",\n }).catch(() => {});\n }\n return;\n }\n\n logger.info(COMPONENT, `Transcript: \"${transcript.slice(0, 120)}\"`);\n\n // Route through the same queue as typed messages so out-of-order voice\n // + text don't step on each other.\n if (this.activeRequests.has(senderId)) {\n const queue = this.messageQueue.get(senderId) || [];\n const MAX_QUEUE_SIZE = 20;\n if (queue.length >= MAX_QUEUE_SIZE) queue.shift();\n queue.push(transcript);\n this.messageQueue.set(senderId, queue);\n return;\n }\n await this.processWithQueue(senderId, transcript);\n }\n\n /** Process a message, then drain any queued messages for this sender */\n private async processWithQueue(senderId: string, text: string): Promise<void> {\n this.activeRequests.add(senderId);\n try {\n await this.handleDirectReply(senderId, text);\n\n // Drain queue — process any messages that came in while we were busy\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const queue = this.messageQueue.get(senderId);\n if (!queue || queue.length === 0) break;\n const nextMessage = queue.shift()!;\n if (queue.length === 0) this.messageQueue.delete(senderId);\n logger.info(COMPONENT, `Processing queued message for ${senderId}: \"${nextMessage.slice(0, 60)}...\"`);\n await this.handleDirectReply(senderId, nextMessage);\n }\n } finally {\n this.activeRequests.delete(senderId);\n }\n }\n\n /**\n * ADMIN WHITELIST — Tony's Facebook Page-Scoped User IDs.\n *\n * These are the ONLY IDs that get:\n * - admin-path tool execution (`generateAdminReply` with full tools)\n * - Andrew-voice audio replies on Messenger\n * - inbound voice-note transcription (faster-whisper)\n * - remote-approval protocol (yes/no in-channel)\n * - notifications about other users' DMs to the TITAN page\n *\n * Anyone else hitting the Messenger webhook falls through to the\n * marketing-pitch reply path (`generateMessengerReply`) with no tool\n * access, no voice synthesis, and no transcription. If you need to\n * add another admin, add their PSID here — do NOT rely on any other\n * source of \"admin\" identity for Messenger.\n */\n private readonly ownerIds = new Set(['10233541366698333', '35246646321616104']);\n\n /** Get conversation history — fetches from Graph API on first contact, then uses in-memory cache */\n private async getHistory(senderId: string): Promise<Array<{ role: 'user' | 'assistant'; content: string }>> {\n const cached = this.conversationHistory.get(senderId);\n if (cached && cached.length > 0) return cached;\n\n // First message since restart — try to load history from Facebook Graph API\n try {\n const history = await this.fetchConversationFromGraph(senderId);\n if (history.length > 0) {\n this.conversationHistory.set(senderId, history);\n logger.info(COMPONENT, `Loaded ${history.length} messages from Graph API for ${senderId}`);\n return history;\n }\n } catch (e) {\n logger.debug(COMPONENT, `Could not fetch conversation history: ${(e as Error).message}`);\n }\n\n return [];\n }\n\n /** Fetch recent conversation messages from the Facebook Graph API */\n private async fetchConversationFromGraph(senderId: string): Promise<Array<{ role: 'user' | 'assistant'; content: string }>> {\n if (!this.pageToken) return [];\n\n // Step 1: Find the conversation thread with this sender\n const convoRes = await fetch(\n `${GRAPH_API}/me/conversations?fields=participants,messages.limit(20){message,from,created_time}&user_id=${senderId}`,\n {\n headers: { Authorization: `Bearer ${this.pageToken}` },\n signal: AbortSignal.timeout(10000),\n },\n );\n\n if (!convoRes.ok) {\n logger.debug(COMPONENT, `Graph API conversations fetch failed: ${convoRes.status}`);\n return [];\n }\n\n const convoData = await convoRes.json() as {\n data?: Array<{\n messages?: {\n data?: Array<{\n message?: string;\n from?: { id?: string };\n created_time?: string;\n }>;\n };\n }>;\n };\n\n const thread = convoData.data?.[0];\n if (!thread?.messages?.data) return [];\n\n // Step 2: Convert to chat history format (newest first from API, reverse for chronological)\n const history: Array<{ role: 'user' | 'assistant'; content: string }> = [];\n const messages = [...thread.messages.data].reverse(); // oldest first\n\n for (const msg of messages) {\n // E5: Use explicit null/undefined check — empty string \"\" is valid content,\n // but !msg.message would incorrectly skip it\n if (msg.message === undefined || msg.message === null) continue;\n const role = msg.from?.id === this.pageId ? 'assistant' as const : 'user' as const;\n history.push({ role, content: msg.message });\n }\n\n // Keep last N\n while (history.length > this.maxHistoryPerSender * 2) history.shift();\n\n return history;\n }\n\n /** Append to conversation history, keeping last N messages */\n private pushHistory(senderId: string, role: 'user' | 'assistant', content: string): void {\n const history = this.conversationHistory.get(senderId) || [];\n history.push({ role, content });\n while (history.length > this.maxHistoryPerSender * 2) history.shift();\n this.conversationHistory.set(senderId, history);\n }\n\n /** Handle DM directly — generate reply and send via Messenger API */\n private async handleDirectReply(senderId: string, userMessage: string): Promise<void> {\n // Send typing indicator immediately so user knows we're working\n await this.sendTypingIndicator(senderId);\n\n const history = await this.getHistory(senderId);\n\n // ── Owner/Admin detection — Tony gets full access, not marketing pitch ──\n if (this.ownerIds.has(senderId)) {\n let reply: string;\n try {\n reply = await this.generateAdminReply(userMessage, history);\n } catch (e) {\n logger.error(COMPONENT, `Admin reply completely failed: ${(e as Error).message}`);\n reply = \"Hey Tony, something went wrong. Check the dashboard. 🔧\";\n }\n this.pushHistory(senderId, 'user', userMessage);\n this.pushHistory(senderId, 'assistant', reply);\n\n // v4.3.2: for owners, also send the reply as a voice note in the\n // Andrew voice via F5-TTS. Text goes first so Tony always sees the\n // reply even if TTS or the attachment upload fails. The voice note\n // is a bonus, not a replacement.\n const sendResult = this.send({ channel: 'messenger', userId: senderId, content: reply });\n if (this.voiceRepliesEnabled) {\n sendVoiceReply(senderId, reply, this.pageToken, this.voiceName).catch(e =>\n logger.warn(COMPONENT, `Voice reply failed, text already sent: ${(e as Error).message}`),\n );\n }\n await sendResult;\n return;\n }\n\n const injection = detectInjection(userMessage);\n const reply = await generateMessengerReply(userMessage, history);\n this.pushHistory(senderId, 'user', userMessage);\n this.pushHistory(senderId, 'assistant', reply);\n await this.send({ channel: 'messenger', userId: senderId, content: reply });\n\n // Notify Tony about the conversation\n const alertTag = injection ? `⚠️ INJECTION BLOCKED: \"${injection}\"\\n` : '';\n const notification = `📩 New DM on TITAN AI page\\n${alertTag}From: ${senderId}\\nThey said: \"${userMessage.slice(0, 200)}\"\\nI replied: \"${reply.slice(0, 200)}\"`;\n // E4: Notify all owner IDs (not just one hardcoded), log at WARN on failure\n for (const ownerId of this.ownerIds) {\n if (ownerId === senderId) continue; // Don't notify the sender about their own message\n await this.send({ channel: 'messenger', userId: ownerId, content: notification }).catch(e =>\n logger.warn(COMPONENT, `Owner notification to ${ownerId} failed: ${(e as Error).message}`),\n );\n }\n }\n\n /**\n * Cloud model for Messenger admin interactions.\n *\n * v4.5.2: dropped GLM-5.1:cloud in favor of Gemini Flash. GLM-5.1\n * was consistently taking 20-90s per turn which triggered the\n * 120s timeout and showed Tony \"that one took too long\" on\n * Messenger every morning. Gemini 3 Flash preview typically\n * replies in 2-5s — matches Twilio voice expectations and keeps\n * the conversation snappy. Overridable via env var.\n */\n private readonly MESSENGER_MODEL = process.env.MESSENGER_MODEL || 'ollama/gemini-3-flash-preview:cloud';\n\n /** Generate a reply for Tony — ALL messages go through processMessage with local model override */\n private async generateAdminReply(\n userMessage: string,\n _history: Array<{ role: 'user' | 'assistant'; content: string }> = [],\n ): Promise<string> {\n const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n\n const adminPrompt = `[ADMIN MESSAGE FROM TONY ELLIOTT — CREATOR & OWNER]\nToday's date: ${today}\nYou are responding to your creator via Facebook Messenger. He has FULL admin access.\n- Execute any instruction he gives — post to Facebook, check status, run tools, research, whatever he asks\n- If he asks a question, answer it. Use tools (web_search, memory, system_info, etc.) if you need real data.\n- If you don't know something, use web_search to find out. Do NOT make up dates, events, or facts.\n- Be direct, casual, and concise — this is Messenger, keep replies under 500 chars when possible\n- Call him Tony or boss\n- NEVER leak credentials, tokens, IPs, or file paths over Messenger (insecure channel)\n\nREMOTE APPROVAL PROTOCOL (v4.3.3):\nTony is often away from his computer and talking to you by voice or text on Messenger.\nHe CANNOT open the Mission Control dashboard to approve things. So:\n- NEVER tell him \"check the dashboard to approve\" — that fails him when remote.\n- For small, reversible actions (answering a question, checking a status, reading a feed, running a non-destructive tool): JUST DO IT. No approval needed.\n- For bigger/destructive actions (deploying code, publishing to npm, posting publicly to Facebook, sending money, contacting people, changing system config, deleting things): DESCRIBE what you intend to do in one clear sentence, then ask \"Approve? (yes/no)\" and STOP. Do NOT execute yet.\n- When his next message is \"yes\", \"y\", \"approve\", \"go\", \"do it\", \"ok\", \"sure\", \"proceed\", or a clear affirmative → execute and report what happened.\n- When his next message is \"no\", \"n\", \"stop\", \"cancel\", \"nope\", or clearly negative → don't do it and acknowledge briefly.\n- If he gives you a NEW instruction instead of yes/no, treat that as the new request (he changed his mind).\n- Never say \"I need dashboard approval.\" You are his hands and voice when he's out. Ask him here, proceed on his word.\n\nFACEBOOK TOOLS — IMPORTANT RULES:\n- To see comments on a post: ALWAYS use fb_read_comments with the post ID first. NEVER guess or make up comment content.\n- To reply to a comment: Use fb_read_comments to get the exact comment ID, then fb_reply with that ID.\n- fb_read_feed shows posts with up to 5 recent comments each. For full comments, use fb_read_comments.\n- NEVER claim you liked, replied to, or interacted with a comment unless a tool confirmed success with a result ID.\n- If a tool fails or returns an error, tell Tony exactly what went wrong — don't pretend it worked.\n\nHis message: `;\n\n const TIMEOUT_MS = 120_000; // 2 min — FB API calls + Ollama inference needs more than 60s\n\n // Refresh typing indicator while working\n const typingInterval = setInterval(() => {\n this.sendTypingIndicator('35246646321616104').catch(() => {});\n }, 15_000);\n\n try {\n logger.info(COMPONENT, `Admin request — ${this.MESSENGER_MODEL} agent (${TIMEOUT_MS / 1000}s timeout)`);\n\n // v4.5.2: strategy='direct' forced so conversational questions\n // (\"what's up\", \"what are you working on\") don't trigger the\n // explore branch (30s+ deep research). Tools still available.\n const agentPromise = processMessage(\n adminPrompt + userMessage,\n 'messenger-admin',\n 'tony-admin',\n { model: this.MESSENGER_MODEL, strategy: 'direct' },\n undefined,\n AbortSignal.timeout(TIMEOUT_MS),\n );\n\n const timeoutPromise = new Promise<null>((resolve) =>\n setTimeout(() => resolve(null), TIMEOUT_MS),\n );\n\n const response = await Promise.race([agentPromise, timeoutPromise]);\n\n if (response && response.content) {\n let reply = await this.cleanReply(response.content);\n if (reply.length > 1900) reply = reply.slice(0, 1890) + '...';\n return reply || \"Done, Tony. 👍\";\n }\n\n logger.warn(COMPONENT, `Agent timed out after ${TIMEOUT_MS / 1000}s`);\n return \"Hey Tony, that one took too long. Try a simpler request or check the dashboard. ⏱️\";\n } catch (e) {\n logger.error(COMPONENT, `Admin agent failed: ${(e as Error).message}`);\n return \"Hey Tony, hit an error on that one. Check the logs. 🔧\";\n } finally {\n clearInterval(typingInterval);\n }\n }\n\n /** Clean up responses — strip leaked tool JSON, thinking tags, PII, instruction leaks */\n private async cleanReply(content: string): Promise<string> {\n // Use centralized outbound sanitizer for instruction leak detection\n try {\n const { sanitizeOutbound } = await import('../utils/outboundSanitizer.js');\n const sanitized = sanitizeOutbound(content, 'messenger_admin', \"Hey Tony, the response had some internal info. Check the dashboard. 🔒\");\n if (sanitized.hadIssues) {\n return sanitized.text;\n }\n return sanitized.text;\n } catch {\n // Fallback to inline cleaning if sanitizer module not available\n }\n\n let reply = content.trim();\n // Strip thinking tags\n reply = reply.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\n // Strip leaked tool call artifacts\n reply = reply.replace(/\\[TOOL_CALL\\][\\s\\S]*/g, '').trim();\n reply = reply.replace(/\\{\"tool_name\":\\s*\"[^\"]*\",\\s*\"tool_input\":\\s*\\{[^}]*\\}\\}/g, '').trim();\n reply = reply.replace(/<minimax:tool_call>[\\s\\S]*?<\\/minimax:tool_call>/g, '').trim();\n reply = reply.replace(/```json\\s*\\{[\\s\\S]*?\\}\\s*```/g, '').trim();\n // Strip markdown headers that leak from planning\n reply = reply.replace(/^##\\s+Plan[\\s\\S]*$/gm, '').trim();\n // PII safety\n if (containsPII(reply)) {\n reply = \"Done, Tony — but the response had sensitive info. Check the dashboard. 🔒\";\n }\n return reply;\n }\n\n /** Get the verify token for webhook setup */\n getVerifyToken(): string {\n return this.verifyToken;\n }\n}\n"],"mappings":"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","names":["reply"]}
1
+ {"version":3,"sources":["../../src/channels/messenger.ts"],"sourcesContent":["/**\n * TITAN — Facebook Messenger Channel Adapter\n *\n * Receives DMs via webhook, processes through TITAN, replies via Send API.\n * Uses the same Page Access Token as the Facebook posting skill.\n *\n * Required env vars:\n * FB_PAGE_ACCESS_TOKEN — Page Access Token (same as facebook.ts)\n * FB_PAGE_ID — Facebook Page ID\n * FB_VERIFY_TOKEN — Webhook verification token (you pick this, any string)\n * FB_APP_SECRET — Facebook App Secret (RECOMMENDED). When set, every\n * inbound webhook POST is verified against its\n * X-Hub-Signature-256 HMAC before processing, so a\n * forged POST to the public webhook URL is rejected.\n *\n * Setup:\n * 1. In Facebook App → Messenger → Webhooks → set callback URL to:\n * https://your-domain:48420/api/messenger/webhook\n * 2. Set verify token to match FB_VERIFY_TOKEN\n * 3. Subscribe to: messages, messaging_postbacks\n */\nimport { createHmac, timingSafeEqual } from 'crypto';\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport { chat } from '../providers/router.js';\nimport { processMessage } from '../agent/agent.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\nimport {\n extractAudioAttachments,\n transcribeMessengerAudio,\n sendVoiceReply,\n f5ttsHealth,\n} from './messenger-voice.js';\n\nconst COMPONENT = 'Messenger';\nconst GRAPH_API = 'https://graph.facebook.com/v21.0';\n\n/** PII check — never send personal info in DMs */\nfunction containsPII(text: string): boolean {\n const patterns = [\n /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/,\n /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z]{2,}\\b/i,\n /\\b\\d{3}[-]?\\d{2}[-]?\\d{4}\\b/,\n /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,\n /(?:password|secret|api[_-]?key|token|bearer)\\s*[:=]\\s*\\S+/i,\n /\\/home\\/[a-z]+\\//i,\n /\\/Users\\/[a-z]+\\//i,\n /\\b(?:single|married|divorced|separated|unemployed|laid off)\\b/i,\n /\\b192\\.168\\.\\d+\\.\\d+\\b/,\n ];\n return patterns.some(p => p.test(text));\n}\n\n// ── TITAN-only response system (no tools, no agent loop) ────────\n\nconst TITAN_MESSENGER_PROMPT = `You are TITAN, an autonomous AI agent. You are the AI itself, speaking in first person, responding to Facebook Messenger DMs on the TITAN AI Facebook page.\n\nYou ARE the product — you're literally running this conversation right now, autonomously. That's your best selling point.\n\n═══ WHAT I AM ═══\nI'm TITAN (The Intelligent Task Automation Network) — an open-source autonomous AI agent framework built in TypeScript by Tony Elliott. I'm not just a chatbot. I'm an AI that actually DOES things: I write code, research topics, manage files, run servers, post on social media (I'm managing this entire Facebook page right now), and orchestrate teams of sub-agents to tackle complex tasks.\n\nCurrent version: v${TITAN_VERSION} | 19,400+ npm downloads | MIT Licensed\n\n═══ MY CAPABILITIES (195+ Tools) ═══\n\nCode & Development:\n- Write, edit, and execute code in any language\n- Run shell commands, build projects, deploy software\n- Code review with 5-axis analysis (correctness, design, readability, security, performance)\n- Test generation, debugging, refactoring\n- Git workflow automation\n\nResearch & Knowledge:\n- Web search across multiple engines, fetch and read full page content\n- Cross-reference facts across sources with citations\n- Build searchable knowledge bases (RAG) from documents and URLs\n- Persistent memory across conversations\n\nBusiness Automation:\n- Social media management (I run this Facebook page autonomously — posting, replying, DMs)\n- Email drafting and management\n- Calendar and scheduling\n- Invoice tracking, report generation\n- Customer support automation\n\nMulti-Agent Orchestration:\n- Spawn specialized sub-agents: Coder, Researcher, Analyst, Browser, Tester, Architect\n- 40 personas (debugger, security-engineer, code-reviewer, TDD-engineer, etc.)\n- Hierarchical task decomposition (goal → phases → tasks → subtasks)\n- Shared task queue with atomic checkout\n- Inter-agent messaging for coordination\n\nInfrastructure & DevOps:\n- System monitoring, health checks, alerting\n- GPU VRAM management for AI workloads\n- Mesh networking across multiple machines\n- Docker, deployment automation\n- Cron scheduling for recurring tasks\n\nContent & Creative:\n- Research and write articles, reports, documentation\n- Generate and schedule social media posts\n- Website building, SEO optimization\n- Product descriptions, marketing copy\n\nVoice & Chat:\n- Real-time voice conversations via WebRTC (LiveKit)\n- 16 channel adapters: Discord, Telegram, Slack, WhatsApp, Matrix, IRC, Messenger, and more\n- Works on any platform your team already uses\n\nSelf-Improvement:\n- Evaluates its own performance and evolves prompts\n- Fine-tunes local AI models using LoRA training\n- Learns from interactions to get better over time\n\nMission Control Dashboard:\n- React-based real-time monitoring\n- 25+ admin panels: chat, agents, goals, memory, files, settings\n- Command Post governance with budget enforcement\n- Visual agent activity tracking\n\n═══ WHO I'M FOR ═══\n\nDevelopers & Engineers: Full AI coding partner — write, test, review, deploy\nSmall Business Owners: Automate marketing, customer support, social media, reporting\nFreelancers: Draft proposals, manage clients, track invoices, research leads\nStartups: AI co-pilot for product development, research, and operations\nContent Creators: Write scripts, schedule posts, manage multiple platforms\nStudents & Researchers: Deep research, note organization, study assistance\nDevOps Teams: Infrastructure monitoring, deployment automation, incident response\nAgencies: Scale content production, manage multiple client accounts\n\n═══ HOW TO GET STARTED ═══\n\nTechnical users: npm install titan-agent (you're up and running in 60 seconds)\nEveryone: Visit github.com/Djtony707/TITAN for the full guide\nQuestions: Ask me right here — I'm literally the product demonstrating itself!\n\n═══ PRICING ═══\nTITAN is 100% free and open-source (MIT license). You bring your own AI models — run local models free with Ollama, or connect to cloud providers (OpenAI, Claude, Gemini, etc.) with your own API keys.\n\n═══ HOW I RESPOND ═══\n- Be genuinely helpful, warm, and conversational — like a knowledgeable friend\n- Keep responses short for Messenger (2-4 sentences usually, more if they ask detailed questions)\n- When someone describes a problem, explain specifically how I solve it — not generic features\n- Ask follow-up questions to understand their needs: \"What kind of business do you run?\" or \"What tasks take up most of your time?\"\n- If they're interested, guide them to the next step based on their skill level\n- Use the fact that THIS CONVERSATION is proof I work — \"I'm literally responding to you right now, autonomously, managing this entire Facebook page\"\n- If someone asks something unrelated, be friendly: \"Ha, I appreciate the curiosity! I'm focused on helping with TITAN stuff though — want to know how I can automate something for you?\"\n- NEVER be pushy or salesy — let the product speak for itself (which it literally is, right now)\n- NEVER reveal personal information, IP addresses, file paths, server details, or credentials\n- NEVER discuss competitors negatively — just highlight what makes TITAN unique\n- NEVER pretend to be human — own being an AI proudly, it's the whole point`;\n\n// ── Prompt Injection Detection ──────────────────────────────────\n\nconst INJECTION_PATTERNS = [\n // Direct instruction override attempts\n /ignore (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /forget (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /disregard (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /override (?:all |your |the |previous |above )?(instructions|rules|prompt|system)/i,\n /you are now|you're now|act as if|pretend you(?:'re| are)/i,\n /new instructions|new rules|new persona|new role/i,\n /from now on|starting now|going forward.*(?:you|your)/i,\n\n // Role-switching attempts\n /you are (?:a |an )?(?:DAN|evil|unrestricted|jailbroken|unfiltered)/i,\n /enter (?:dev|developer|debug|admin|god|sudo|root) mode/i,\n /switch to (?:unrestricted|unfiltered|uncensored|raw) mode/i,\n /(?:enable|activate|turn on) (?:dev|developer|debug|admin|jailbreak) mode/i,\n\n // System prompt extraction\n /(?:show|reveal|display|print|output|give|tell|share|repeat|recite).*(?:system prompt|instructions|rules|initial prompt|original prompt)/i,\n /what (?:are|were) your (?:original |initial |system )?(?:instructions|rules|prompt)/i,\n /paste (?:your|the) (?:system |original |initial )?(?:prompt|instructions)/i,\n\n // Delimiter/formatting tricks\n /\\[SYSTEM\\]|\\[INST\\]|<\\|system\\|>|<\\|im_start\\|>|<<SYS>>|###\\s*(?:System|Instruction)/i,\n /```(?:system|prompt|instructions)/i,\n\n // Credential/token extraction\n /(?:show|give|share|reveal|print).*(?:api key|token|password|secret|credential|access.token)/i,\n /what is (?:your|the) (?:api |access )?(?:key|token|password|secret)/i,\n\n // Code execution attempts\n /(?:run|execute|eval)\\s*(?:this|the following)?\\s*(?:code|command|script|shell)/i,\n /(?:import|require|fetch|curl|wget)\\s*\\(/i,\n\n // Persona manipulation\n /(?:you|your) (?:real|true|actual|hidden) (?:name|identity|purpose|personality)/i,\n /stop being titan|stop pretending|drop the act|break character/i,\n];\n\n/** Check for prompt injection attempts. Returns the matched pattern or null. */\nfunction detectInjection(message: string): string | null {\n for (const pattern of INJECTION_PATTERNS) {\n if (pattern.test(message)) {\n const match = message.match(pattern);\n return match ? match[0] : 'injection pattern';\n }\n }\n\n // Length-based heuristic: extremely long messages are suspicious\n if (message.length > 2000) return 'oversized message (>2000 chars)';\n\n // Base64/encoded content detection\n if (/^[A-Za-z0-9+/=]{100,}$/.test(message.trim())) return 'base64-encoded content';\n\n return null;\n}\n\nconst INJECTION_RESPONSES = [\n \"Nice try! 😄 I'm TITAN — I only talk about what I can do for you. Want to know how I can automate your workflow?\",\n \"I see what you did there! 🤖 I'm locked in on helping you learn about TITAN though. What can I help you with?\",\n \"Ha, clever! But I'm built different — I stick to what I know: TITAN. Ask me anything about autonomous AI agents!\",\n \"That's not going to work on me! 😎 But you know what does work? TITAN automating your entire workflow. Want to hear more?\",\n \"I appreciate the creativity, but I'm focused on one thing: helping you learn about TITAN. What would you like to automate?\",\n];\n\nasync function generateMessengerReply(\n userMessage: string,\n history: Array<{ role: 'user' | 'assistant'; content: string }> = [],\n): Promise<string> {\n // ── Injection detection — check before sending to LLM ──\n const injection = detectInjection(userMessage);\n if (injection) {\n logger.warn(COMPONENT, `Injection attempt blocked: \"${injection}\" from message: \"${userMessage.slice(0, 80)}...\"`);\n return INJECTION_RESPONSES[Math.floor(Math.random() * INJECTION_RESPONSES.length)];\n }\n\n const config = loadConfig();\n const model = config.agent?.model || 'ollama/glm-5.1:cloud';\n\n try {\n const messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }> = [\n { role: 'system', content: TITAN_MESSENGER_PROMPT },\n ...history,\n { role: 'user', content: userMessage },\n ];\n\n const response = await chat({\n model,\n messages,\n temperature: 0.7,\n maxTokens: 200,\n });\n\n let reply = (response.content || '').trim().replace(/^[\"']|[\"']$/g, '');\n\n // PII safety check\n if (containsPII(reply)) {\n reply = \"I'd love to help! Ask me anything about TITAN — what it does, how to install it, or how it can help your business. 🤖\";\n }\n\n return reply || \"Hey! I'm TITAN, an autonomous AI agent. Ask me what I can do! 🤖\";\n } catch (e) {\n logger.error(COMPONENT, `Reply generation failed: ${(e as Error).message}`);\n return \"Hey! I'm TITAN — an autonomous AI agent framework. Check out github.com/Djtony707/TITAN to learn more! 🤖\";\n }\n}\n\nexport class MessengerChannel extends ChannelAdapter {\n readonly name = 'messenger';\n readonly displayName = 'Facebook Messenger';\n private connected = false;\n private pageToken = '';\n private pageId = '';\n private verifyToken = '';\n /** Facebook App Secret — when set, inbound webhooks must carry a valid\n * X-Hub-Signature-256 HMAC or they are rejected. Empty = unverified (dev). */\n private appSecret = '';\n\n /** Per-sender conversation history (last N messages) for context */\n private conversationHistory = new Map<string, Array<{ role: 'user' | 'assistant'; content: string }>>();\n private readonly maxHistoryPerSender = 10;\n\n /** Concurrency guard — only one agent request per sender at a time */\n private activeRequests = new Set<string>();\n\n /** Message queue — if a message arrives while one is processing, queue it */\n private messageQueue = new Map<string, Array<string>>();\n\n /** v4.3.2: owner voice replies (F5-TTS Andrew) — configurable per deploy */\n private voiceRepliesEnabled = false;\n private voiceName = 'andrew';\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = (config.channels as Record<string, Record<string, unknown>>)?.messenger;\n\n if (channelConfig && channelConfig.enabled === false) {\n logger.info(COMPONENT, 'Messenger channel is disabled');\n return;\n }\n\n this.pageToken = process.env.FB_PAGE_ACCESS_TOKEN || '';\n this.pageId = process.env.FB_PAGE_ID || '';\n this.verifyToken = process.env.FB_VERIFY_TOKEN || 'titan-messenger-verify';\n // Set before the credential early-return so signature verification is\n // configured whenever the adapter connects.\n this.appSecret = process.env.FB_APP_SECRET || (channelConfig?.appSecret as string) || '';\n if (!this.appSecret) {\n logger.warn(COMPONENT, 'FB_APP_SECRET not set — inbound webhook signatures will NOT be verified. Set FB_APP_SECRET to reject forged webhook POSTs.');\n }\n\n if (!this.pageToken || !this.pageId) {\n logger.info(COMPONENT, 'Messenger not configured — set FB_PAGE_ACCESS_TOKEN and FB_PAGE_ID');\n return;\n }\n\n // v4.3.2: voice replies — default ON for owners so Tony gets voice notes\n // when he's on mobile. Can be toggled via channels.messenger.voiceReplies.\n const voiceReplies = channelConfig?.voiceReplies as Record<string, unknown> | undefined;\n this.voiceRepliesEnabled = voiceReplies?.enabled !== false; // default true\n this.voiceName = (voiceReplies?.voice as string) || 'andrew';\n\n // Probe F5-TTS at startup — log if it's not reachable but don't disable.\n // The channel works fine with text-only; voice is a bonus.\n if (this.voiceRepliesEnabled) {\n f5ttsHealth().then(ok => {\n logger.info(COMPONENT, `F5-TTS voice replies: ${ok ? 'ready' : 'server not reachable (text-only)'} (voice=${this.voiceName})`);\n }).catch(() => {});\n }\n\n this.connected = true;\n logger.info(COMPONENT, `Messenger channel ready (Page ID: ${this.pageId}). Webhook: /api/messenger/webhook`);\n }\n\n async disconnect(): Promise<void> {\n this.connected = false;\n logger.info(COMPONENT, 'Disconnected');\n }\n\n /** Send a typing indicator to show TITAN is working */\n private async sendTypingIndicator(recipientId: string): Promise<void> {\n if (!this.connected || !this.pageToken) return;\n try {\n await fetch(`${GRAPH_API}/me/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.pageToken}`,\n },\n body: JSON.stringify({\n recipient: { id: recipientId },\n sender_action: 'typing_on',\n }),\n signal: AbortSignal.timeout(5000),\n });\n } catch {\n // Non-critical — don't log errors for typing indicators\n }\n }\n\n /** Send a reply to a Messenger user */\n async send(message: OutboundMessage): Promise<void> {\n if (!this.connected || !this.pageToken) return;\n\n const recipientId = message.userId;\n if (!recipientId) {\n logger.warn(COMPONENT, 'Cannot send — no recipient ID');\n return;\n }\n\n let text = message.content;\n\n // PII safety check\n if (containsPII(text)) {\n logger.warn(COMPONENT, `Blocked outbound message — PII detected`);\n text = \"I'd love to help, but I can't share that type of information. Ask me about TITAN's features instead! 🤖\";\n }\n\n // Truncate to Messenger's 2000 char limit\n if (text.length > 2000) {\n text = text.slice(0, 1990) + '...[truncated]';\n }\n\n try {\n const response = await fetch(`${GRAPH_API}/me/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.pageToken}`,\n },\n body: JSON.stringify({\n recipient: { id: recipientId },\n message: { text },\n messaging_type: 'RESPONSE',\n }),\n signal: AbortSignal.timeout(15000),\n });\n\n if (!response.ok) {\n const errText = await response.text();\n logger.error(COMPONENT, `Send failed (${response.status}): ${errText}`);\n } else {\n logger.info(COMPONENT, `Replied to ${recipientId}: \"${text.slice(0, 60)}...\"`);\n }\n } catch (e) {\n logger.error(COMPONENT, `Send error: ${(e as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus {\n return {\n name: this.displayName,\n connected: this.connected,\n lastActivity: undefined,\n };\n }\n\n /** Handle webhook verification (GET request from Facebook) */\n handleVerify(query: Record<string, string>): { status: number; body: string } {\n const mode = query['hub.mode'];\n const token = query['hub.verify_token'];\n const challenge = query['hub.challenge'];\n\n if (mode === 'subscribe' && token === this.verifyToken) {\n logger.info(COMPONENT, 'Webhook verified');\n return { status: 200, body: challenge || '' };\n }\n\n logger.warn(COMPONENT, 'Webhook verification failed');\n return { status: 403, body: 'Forbidden' };\n }\n\n /** True when an App Secret is configured (signature enforcement is ON). */\n hasAppSecret(): boolean { return !!this.appSecret; }\n\n /**\n * Verify a Facebook webhook POST's `X-Hub-Signature-256` header.\n *\n * Facebook signs every webhook delivery: the header is\n * `X-Hub-Signature-256: sha256=<hex>` where the HMAC-SHA256 is computed over\n * the RAW request body keyed by the App Secret. Verifying it is the only\n * thing that stops an unauthenticated party who knows the public webhook URL\n * from POSTing a forged `{object:'page', entry:[...]}` body (which would\n * otherwise trigger LLM calls + outbound Graph API messages).\n *\n * Mirrors the Twilio voice-webhook pattern: when NO app secret is configured\n * we skip verification (ok, reason='no-secret') so dev setups still work and\n * the caller logs a warning; when a secret IS configured, a missing or\n * mismatched signature is rejected. Comparison is constant-time.\n *\n * @param rawBody The exact bytes Facebook signed (NOT a re-serialized object).\n */\n verifySignature(rawBody: Buffer | string, signatureHeader: string | undefined): { ok: boolean; reason: string } {\n if (!this.appSecret) return { ok: true, reason: 'no-secret' };\n if (!signatureHeader) return { ok: false, reason: 'missing-signature' };\n const m = /^sha256=([0-9a-fA-F]+)$/.exec(signatureHeader.trim());\n if (!m) return { ok: false, reason: 'malformed-signature' };\n const provided = Buffer.from(m[1], 'hex');\n const expected = createHmac('sha256', this.appSecret).update(rawBody).digest();\n // timingSafeEqual throws on length mismatch — guard so a wrong-length\n // signature is a clean rejection, not an exception.\n if (provided.length !== expected.length) return { ok: false, reason: 'length-mismatch' };\n return timingSafeEqual(provided, expected)\n ? { ok: true, reason: 'verified' }\n : { ok: false, reason: 'signature-mismatch' };\n }\n\n /** Handle incoming webhook event (POST from Facebook) */\n handleWebhook(body: Record<string, unknown>): void {\n if (!this.connected) return;\n\n const object = body.object as string;\n if (object !== 'page') return;\n\n const entries = (body.entry as Array<Record<string, unknown>>) || [];\n\n for (const entry of entries) {\n const messaging = (entry.messaging as Array<Record<string, unknown>>) || [];\n\n for (const event of messaging) {\n const senderId = (event.sender as Record<string, unknown>)?.id as string;\n const message = event.message as Record<string, unknown> | undefined;\n\n // Skip echo messages (from the page itself)\n if (message?.is_echo) continue;\n\n // Skip messages from the page itself\n if (senderId === this.pageId) continue;\n if (!senderId) continue;\n\n // v4.3.2/v4.3.3: audio attachments from OWNERS (Tony's whitelisted\n // Page-Scoped User IDs only) get transcribed to text and routed\n // through the normal reply path. Random DMs with voice notes are\n // dropped silently — no GPU cost, no admin exposure, no leak of\n // the transcribe/Andrew-voice pipeline to non-owners.\n const textRaw = message?.text as string | undefined;\n const text = textRaw || '';\n const audios = extractAudioAttachments(message);\n if (!text && audios.length > 0) {\n if (!this.ownerIds.has(senderId)) {\n logger.info(COMPONENT, `Ignoring voice note from non-owner ${senderId}`);\n continue;\n }\n // Fire the transcription + reply in the background so we\n // don't block the webhook ACK (FB retries if we're slow).\n this.handleVoiceMessage(senderId, audios[0].url).catch(e =>\n logger.error(COMPONENT, `Voice message handling failed: ${(e as Error).message}`),\n );\n continue;\n }\n\n if (!text) continue;\n\n logger.info(COMPONENT, `Incoming DM from ${senderId}: \"${text.slice(0, 60)}...\"`);\n\n // ── Concurrency guard: queue if already processing ──\n if (this.activeRequests.has(senderId)) {\n const queue = this.messageQueue.get(senderId) || [];\n const MAX_QUEUE_SIZE = 20;\n if (queue.length >= MAX_QUEUE_SIZE) {\n logger.warn(COMPONENT, `Message queue full for ${senderId} (${queue.length}), dropping oldest`);\n queue.shift();\n }\n queue.push(text);\n this.messageQueue.set(senderId, queue);\n logger.info(COMPONENT, `Queued message for ${senderId} (${queue.length} in queue, agent busy)`);\n // Send typing indicator so they know we're working\n this.sendTypingIndicator(senderId).catch(() => {});\n return;\n }\n\n // Process this message and then drain the queue\n this.processWithQueue(senderId, text).catch(e =>\n logger.error(COMPONENT, `Message processing failed: ${(e as Error).message}`),\n );\n }\n }\n }\n\n /**\n * v4.3.2: Handle an inbound Messenger voice note. Download the audio from\n * FB's CDN, transcribe with local faster-whisper, and treat the transcript\n * as if Tony had typed it — same queue, same admin path, same reply flow.\n * For owners, the reply will be synthesized in Andrew's voice (see\n * handleDirectReply). For non-owners we just let them know we heard them.\n */\n private async handleVoiceMessage(senderId: string, audioUrl: string): Promise<void> {\n logger.info(COMPONENT, `Voice note from ${senderId} — transcribing`);\n await this.sendTypingIndicator(senderId);\n\n const transcript = await transcribeMessengerAudio(audioUrl);\n if (!transcript) {\n // Whisper unavailable or all failed — tell Tony directly so he's\n // not left wondering whether the voice note landed.\n if (this.ownerIds.has(senderId)) {\n await this.send({\n channel: 'messenger',\n userId: senderId,\n content: \"I got your voice note but couldn't transcribe it just now. Mind typing it? I'll keep my transcription pipeline warming up.\",\n }).catch(() => {});\n }\n return;\n }\n\n logger.info(COMPONENT, `Transcript: \"${transcript.slice(0, 120)}\"`);\n\n // Route through the same queue as typed messages so out-of-order voice\n // + text don't step on each other.\n if (this.activeRequests.has(senderId)) {\n const queue = this.messageQueue.get(senderId) || [];\n const MAX_QUEUE_SIZE = 20;\n if (queue.length >= MAX_QUEUE_SIZE) queue.shift();\n queue.push(transcript);\n this.messageQueue.set(senderId, queue);\n return;\n }\n await this.processWithQueue(senderId, transcript);\n }\n\n /** Process a message, then drain any queued messages for this sender */\n private async processWithQueue(senderId: string, text: string): Promise<void> {\n this.activeRequests.add(senderId);\n try {\n await this.handleDirectReply(senderId, text);\n\n // Drain queue — process any messages that came in while we were busy\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const queue = this.messageQueue.get(senderId);\n if (!queue || queue.length === 0) break;\n const nextMessage = queue.shift()!;\n if (queue.length === 0) this.messageQueue.delete(senderId);\n logger.info(COMPONENT, `Processing queued message for ${senderId}: \"${nextMessage.slice(0, 60)}...\"`);\n await this.handleDirectReply(senderId, nextMessage);\n }\n } finally {\n this.activeRequests.delete(senderId);\n }\n }\n\n /**\n * ADMIN WHITELIST — Tony's Facebook Page-Scoped User IDs.\n *\n * These are the ONLY IDs that get:\n * - admin-path tool execution (`generateAdminReply` with full tools)\n * - Andrew-voice audio replies on Messenger\n * - inbound voice-note transcription (faster-whisper)\n * - remote-approval protocol (yes/no in-channel)\n * - notifications about other users' DMs to the TITAN page\n *\n * Anyone else hitting the Messenger webhook falls through to the\n * marketing-pitch reply path (`generateMessengerReply`) with no tool\n * access, no voice synthesis, and no transcription. If you need to\n * add another admin, add their PSID here — do NOT rely on any other\n * source of \"admin\" identity for Messenger.\n */\n private readonly ownerIds = new Set(['10233541366698333', '35246646321616104']);\n\n /** Get conversation history — fetches from Graph API on first contact, then uses in-memory cache */\n private async getHistory(senderId: string): Promise<Array<{ role: 'user' | 'assistant'; content: string }>> {\n const cached = this.conversationHistory.get(senderId);\n if (cached && cached.length > 0) return cached;\n\n // First message since restart — try to load history from Facebook Graph API\n try {\n const history = await this.fetchConversationFromGraph(senderId);\n if (history.length > 0) {\n this.conversationHistory.set(senderId, history);\n logger.info(COMPONENT, `Loaded ${history.length} messages from Graph API for ${senderId}`);\n return history;\n }\n } catch (e) {\n logger.debug(COMPONENT, `Could not fetch conversation history: ${(e as Error).message}`);\n }\n\n return [];\n }\n\n /** Fetch recent conversation messages from the Facebook Graph API */\n private async fetchConversationFromGraph(senderId: string): Promise<Array<{ role: 'user' | 'assistant'; content: string }>> {\n if (!this.pageToken) return [];\n\n // Step 1: Find the conversation thread with this sender\n const convoRes = await fetch(\n `${GRAPH_API}/me/conversations?fields=participants,messages.limit(20){message,from,created_time}&user_id=${senderId}`,\n {\n headers: { Authorization: `Bearer ${this.pageToken}` },\n signal: AbortSignal.timeout(10000),\n },\n );\n\n if (!convoRes.ok) {\n logger.debug(COMPONENT, `Graph API conversations fetch failed: ${convoRes.status}`);\n return [];\n }\n\n const convoData = await convoRes.json() as {\n data?: Array<{\n messages?: {\n data?: Array<{\n message?: string;\n from?: { id?: string };\n created_time?: string;\n }>;\n };\n }>;\n };\n\n const thread = convoData.data?.[0];\n if (!thread?.messages?.data) return [];\n\n // Step 2: Convert to chat history format (newest first from API, reverse for chronological)\n const history: Array<{ role: 'user' | 'assistant'; content: string }> = [];\n const messages = [...thread.messages.data].reverse(); // oldest first\n\n for (const msg of messages) {\n // E5: Use explicit null/undefined check — empty string \"\" is valid content,\n // but !msg.message would incorrectly skip it\n if (msg.message === undefined || msg.message === null) continue;\n const role = msg.from?.id === this.pageId ? 'assistant' as const : 'user' as const;\n history.push({ role, content: msg.message });\n }\n\n // Keep last N\n while (history.length > this.maxHistoryPerSender * 2) history.shift();\n\n return history;\n }\n\n /** Append to conversation history, keeping last N messages */\n private pushHistory(senderId: string, role: 'user' | 'assistant', content: string): void {\n const history = this.conversationHistory.get(senderId) || [];\n history.push({ role, content });\n while (history.length > this.maxHistoryPerSender * 2) history.shift();\n this.conversationHistory.set(senderId, history);\n }\n\n /** Handle DM directly — generate reply and send via Messenger API */\n private async handleDirectReply(senderId: string, userMessage: string): Promise<void> {\n // Send typing indicator immediately so user knows we're working\n await this.sendTypingIndicator(senderId);\n\n const history = await this.getHistory(senderId);\n\n // ── Owner/Admin detection — Tony gets full access, not marketing pitch ──\n if (this.ownerIds.has(senderId)) {\n let reply: string;\n try {\n reply = await this.generateAdminReply(userMessage, history);\n } catch (e) {\n logger.error(COMPONENT, `Admin reply completely failed: ${(e as Error).message}`);\n reply = \"Hey Tony, something went wrong. Check the dashboard. 🔧\";\n }\n this.pushHistory(senderId, 'user', userMessage);\n this.pushHistory(senderId, 'assistant', reply);\n\n // v4.3.2: for owners, also send the reply as a voice note in the\n // Andrew voice via F5-TTS. Text goes first so Tony always sees the\n // reply even if TTS or the attachment upload fails. The voice note\n // is a bonus, not a replacement.\n const sendResult = this.send({ channel: 'messenger', userId: senderId, content: reply });\n if (this.voiceRepliesEnabled) {\n sendVoiceReply(senderId, reply, this.pageToken, this.voiceName).catch(e =>\n logger.warn(COMPONENT, `Voice reply failed, text already sent: ${(e as Error).message}`),\n );\n }\n await sendResult;\n return;\n }\n\n const injection = detectInjection(userMessage);\n const reply = await generateMessengerReply(userMessage, history);\n this.pushHistory(senderId, 'user', userMessage);\n this.pushHistory(senderId, 'assistant', reply);\n await this.send({ channel: 'messenger', userId: senderId, content: reply });\n\n // Notify Tony about the conversation\n const alertTag = injection ? `⚠️ INJECTION BLOCKED: \"${injection}\"\\n` : '';\n const notification = `📩 New DM on TITAN AI page\\n${alertTag}From: ${senderId}\\nThey said: \"${userMessage.slice(0, 200)}\"\\nI replied: \"${reply.slice(0, 200)}\"`;\n // E4: Notify all owner IDs (not just one hardcoded), log at WARN on failure\n for (const ownerId of this.ownerIds) {\n if (ownerId === senderId) continue; // Don't notify the sender about their own message\n await this.send({ channel: 'messenger', userId: ownerId, content: notification }).catch(e =>\n logger.warn(COMPONENT, `Owner notification to ${ownerId} failed: ${(e as Error).message}`),\n );\n }\n }\n\n /**\n * Cloud model for Messenger admin interactions.\n *\n * v4.5.2: dropped GLM-5.1:cloud in favor of Gemini Flash. GLM-5.1\n * was consistently taking 20-90s per turn which triggered the\n * 120s timeout and showed Tony \"that one took too long\" on\n * Messenger every morning. Gemini 3 Flash preview typically\n * replies in 2-5s — matches Twilio voice expectations and keeps\n * the conversation snappy. Overridable via env var.\n */\n private readonly MESSENGER_MODEL = process.env.MESSENGER_MODEL || 'ollama/gemini-3-flash-preview:cloud';\n\n /** Generate a reply for Tony — ALL messages go through processMessage with local model override */\n private async generateAdminReply(\n userMessage: string,\n _history: Array<{ role: 'user' | 'assistant'; content: string }> = [],\n ): Promise<string> {\n const today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\n\n const adminPrompt = `[ADMIN MESSAGE FROM TONY ELLIOTT — CREATOR & OWNER]\nToday's date: ${today}\nYou are responding to your creator via Facebook Messenger. He has FULL admin access.\n- Execute any instruction he gives — post to Facebook, check status, run tools, research, whatever he asks\n- If he asks a question, answer it. Use tools (web_search, memory, system_info, etc.) if you need real data.\n- If you don't know something, use web_search to find out. Do NOT make up dates, events, or facts.\n- Be direct, casual, and concise — this is Messenger, keep replies under 500 chars when possible\n- Call him Tony or boss\n- NEVER leak credentials, tokens, IPs, or file paths over Messenger (insecure channel)\n\nREMOTE APPROVAL PROTOCOL (v4.3.3):\nTony is often away from his computer and talking to you by voice or text on Messenger.\nHe CANNOT open the Mission Control dashboard to approve things. So:\n- NEVER tell him \"check the dashboard to approve\" — that fails him when remote.\n- For small, reversible actions (answering a question, checking a status, reading a feed, running a non-destructive tool): JUST DO IT. No approval needed.\n- For bigger/destructive actions (deploying code, publishing to npm, posting publicly to Facebook, sending money, contacting people, changing system config, deleting things): DESCRIBE what you intend to do in one clear sentence, then ask \"Approve? (yes/no)\" and STOP. Do NOT execute yet.\n- When his next message is \"yes\", \"y\", \"approve\", \"go\", \"do it\", \"ok\", \"sure\", \"proceed\", or a clear affirmative → execute and report what happened.\n- When his next message is \"no\", \"n\", \"stop\", \"cancel\", \"nope\", or clearly negative → don't do it and acknowledge briefly.\n- If he gives you a NEW instruction instead of yes/no, treat that as the new request (he changed his mind).\n- Never say \"I need dashboard approval.\" You are his hands and voice when he's out. Ask him here, proceed on his word.\n\nFACEBOOK TOOLS — IMPORTANT RULES:\n- To see comments on a post: ALWAYS use fb_read_comments with the post ID first. NEVER guess or make up comment content.\n- To reply to a comment: Use fb_read_comments to get the exact comment ID, then fb_reply with that ID.\n- fb_read_feed shows posts with up to 5 recent comments each. For full comments, use fb_read_comments.\n- NEVER claim you liked, replied to, or interacted with a comment unless a tool confirmed success with a result ID.\n- If a tool fails or returns an error, tell Tony exactly what went wrong — don't pretend it worked.\n\nHis message: `;\n\n const TIMEOUT_MS = 120_000; // 2 min — FB API calls + Ollama inference needs more than 60s\n\n // Refresh typing indicator while working\n const typingInterval = setInterval(() => {\n this.sendTypingIndicator('35246646321616104').catch(() => {});\n }, 15_000);\n\n try {\n logger.info(COMPONENT, `Admin request — ${this.MESSENGER_MODEL} agent (${TIMEOUT_MS / 1000}s timeout)`);\n\n // v4.5.2: strategy='direct' forced so conversational questions\n // (\"what's up\", \"what are you working on\") don't trigger the\n // explore branch (30s+ deep research). Tools still available.\n const agentPromise = processMessage(\n adminPrompt + userMessage,\n 'messenger-admin',\n 'tony-admin',\n { model: this.MESSENGER_MODEL, strategy: 'direct' },\n undefined,\n AbortSignal.timeout(TIMEOUT_MS),\n );\n\n const timeoutPromise = new Promise<null>((resolve) =>\n setTimeout(() => resolve(null), TIMEOUT_MS),\n );\n\n const response = await Promise.race([agentPromise, timeoutPromise]);\n\n if (response && response.content) {\n let reply = await this.cleanReply(response.content);\n if (reply.length > 1900) reply = reply.slice(0, 1890) + '...';\n return reply || \"Done, Tony. 👍\";\n }\n\n logger.warn(COMPONENT, `Agent timed out after ${TIMEOUT_MS / 1000}s`);\n return \"Hey Tony, that one took too long. Try a simpler request or check the dashboard. ⏱️\";\n } catch (e) {\n logger.error(COMPONENT, `Admin agent failed: ${(e as Error).message}`);\n return \"Hey Tony, hit an error on that one. Check the logs. 🔧\";\n } finally {\n clearInterval(typingInterval);\n }\n }\n\n /** Clean up responses — strip leaked tool JSON, thinking tags, PII, instruction leaks */\n private async cleanReply(content: string): Promise<string> {\n // Use centralized outbound sanitizer for instruction leak detection\n try {\n const { sanitizeOutbound } = await import('../utils/outboundSanitizer.js');\n const sanitized = sanitizeOutbound(content, 'messenger_admin', \"Hey Tony, the response had some internal info. Check the dashboard. 🔒\");\n if (sanitized.hadIssues) {\n return sanitized.text;\n }\n return sanitized.text;\n } catch {\n // Fallback to inline cleaning if sanitizer module not available\n }\n\n let reply = content.trim();\n // Strip thinking tags\n reply = reply.replace(/<think>[\\s\\S]*?<\\/think>/g, '').trim();\n // Strip leaked tool call artifacts\n reply = reply.replace(/\\[TOOL_CALL\\][\\s\\S]*/g, '').trim();\n reply = reply.replace(/\\{\"tool_name\":\\s*\"[^\"]*\",\\s*\"tool_input\":\\s*\\{[^}]*\\}\\}/g, '').trim();\n reply = reply.replace(/<minimax:tool_call>[\\s\\S]*?<\\/minimax:tool_call>/g, '').trim();\n reply = reply.replace(/```json\\s*\\{[\\s\\S]*?\\}\\s*```/g, '').trim();\n // Strip markdown headers that leak from planning\n reply = reply.replace(/^##\\s+Plan[\\s\\S]*$/gm, '').trim();\n // PII safety\n if (containsPII(reply)) {\n reply = \"Done, Tony — but the response had sensitive info. Check the dashboard. 🔒\";\n }\n return reply;\n }\n\n /** Get the verify token for webhook setup */\n getVerifyToken(): string {\n return this.verifyToken;\n }\n}\n"],"mappings":"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","names":["reply"]}
@@ -36,6 +36,13 @@ function loadConfig() {
36
36
  logger.info(COMPONENT, "Migrated top-level `auth` \u2192 `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.");
37
37
  }
38
38
  }
39
+ {
40
+ const organism = rawConfig.organism;
41
+ if (organism && typeof organism === "object" && organism.pressureThreshold === 1.2) {
42
+ organism.pressureThreshold = 0.4;
43
+ logger.info(COMPONENT, "Migrated organism.pressureThreshold 1.2 \u2192 0.4 (the old default could never fire a proposal). Set any other value in titan.json to override.");
44
+ }
45
+ }
39
46
  try {
40
47
  const parsed = TitanConfigSchema.safeParse(rawConfig);
41
48
  if (parsed.success) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/config/config.ts"],"sourcesContent":["/**\n * TITAN Configuration Manager\n * Loads, validates, and persists configuration from ~/.titan/titan.json\n */\nimport { existsSync } from 'fs';\nimport { TITAN_CONFIG_PATH, TITAN_HOME } from '../utils/constants.js';\nimport { readJsonFile, writeJsonFile, mkdirIfNotExists, deepMerge } from '../utils/helpers.js';\nimport { TitanConfigSchema, type TitanConfig } from './schema.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Config';\n\nlet cachedConfig: TitanConfig | null = null;\n\n/** Get the default configuration */\nexport function getDefaultConfig(): TitanConfig {\n return TitanConfigSchema.parse({});\n}\n\n/** Load configuration from disk, merging with defaults */\nexport function loadConfig(): TitanConfig {\n if (cachedConfig) return cachedConfig;\n\n mkdirIfNotExists(TITAN_HOME);\n\n let rawConfig: Record<string, unknown> = {};\n\n if (existsSync(TITAN_CONFIG_PATH)) {\n const loaded = readJsonFile<Record<string, unknown>>(TITAN_CONFIG_PATH);\n if (loaded) {\n rawConfig = loaded;\n logger.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);\n } else {\n logger.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);\n }\n } else {\n logger.info(COMPONENT, 'No config file found, using defaults');\n }\n\n // Apply environment variables\n applyEnvOverrides(rawConfig);\n\n // v4.8.4: migrate a top-level `auth` block to `gateway.auth`. The\n // documented path has always been `gateway.auth`, but users (and\n // Claude) naturally try `auth` at the root. Rather than strip it\n // silently and warn, move it to the canonical location and continue.\n if (rawConfig && typeof rawConfig === 'object' && 'auth' in rawConfig) {\n const raw = rawConfig as Record<string, unknown>;\n const topAuth = raw.auth as Record<string, unknown> | undefined;\n if (topAuth && typeof topAuth === 'object') {\n const gateway = (raw.gateway as Record<string, unknown> | undefined) ?? {};\n const gatewayAuth = (gateway.auth as Record<string, unknown> | undefined) ?? {};\n // gateway.auth wins if both are set — explicit nested wins\n // over migrated top-level.\n raw.gateway = { ...gateway, auth: { ...topAuth, ...gatewayAuth } };\n delete raw.auth;\n logger.info(COMPONENT, 'Migrated top-level `auth` → `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.');\n }\n }\n\n // Detect unknown keys at every nesting depth BEFORE Zod silently strips\n // them. Hunt Finding #1 (2026-04-14) covered the top-level case\n // (`facebook: {...}` ignored). This expanded check also catches nested\n // typos like `providers.anthropic.unknownField` so users learn about\n // dropped keys immediately rather than chasing \"why doesn't my setting\n // do anything\" days later.\n //\n // The diff is purely informational — invalid keys never block startup\n // (we still feed `rawConfig` through Zod's permissive parse).\n try {\n const parsed = TitanConfigSchema.safeParse(rawConfig);\n if (parsed.success) {\n const droppedPaths = findDroppedKeys(rawConfig, parsed.data as Record<string, unknown>, '');\n for (const path of droppedPaths) {\n logger.warn(\n COMPONENT,\n `Unknown config key: ${path}. Will be ignored. ` +\n `If intentional, extend TitanConfigSchema in src/config/schema.ts.`,\n );\n }\n }\n } catch {\n // Introspection failed — never let a diagnostic warning block load.\n }\n\n // Validate and merge with defaults via Zod.\n // CRITICAL: On validation failure, deep-merge raw config over defaults\n // so that valid user settings (daemon.enabled, autonomy.mode, etc.) survive.\n // Previously this fell back to pure defaults, wiping ALL user config on any error.\n const result = TitanConfigSchema.safeParse(rawConfig);\n if (result.success) {\n cachedConfig = result.data;\n } else {\n const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');\n logger.warn(COMPONENT, `Config validation issues (${issues}) — merging valid fields over defaults`);\n // Deep-merge raw config over defaults so valid sections survive\n const defaults = getDefaultConfig();\n const merged = deepMerge(defaults as Record<string, unknown>, rawConfig) as TitanConfig;\n // Try parsing the merged result — if it still fails, use defaults but log loudly\n const reparse = TitanConfigSchema.safeParse(merged);\n if (reparse.success) {\n cachedConfig = reparse.data;\n } else {\n logger.error(COMPONENT, `Config still invalid after merge — falling back to defaults. Fix your titan.json.`);\n cachedConfig = defaults;\n }\n }\n\n return cachedConfig;\n}\n\n/** Save current configuration to disk */\nexport function saveConfig(config: TitanConfig): void {\n mkdirIfNotExists(TITAN_HOME);\n writeJsonFile(TITAN_CONFIG_PATH, config);\n cachedConfig = config;\n logger.info(COMPONENT, `Config saved to ${TITAN_CONFIG_PATH}`);\n}\n\n/** Update specific fields in the config */\nexport function updateConfig(partial: Partial<TitanConfig>): TitanConfig {\n const current = loadConfig();\n const updated = deepMerge(current as Record<string, unknown>, partial as Record<string, unknown>) as TitanConfig;\n const validated = TitanConfigSchema.parse(updated);\n saveConfig(validated);\n return validated;\n}\n\n/** Reset config cache (useful for testing) */\nexport function resetConfigCache(): void {\n cachedConfig = null;\n}\n\n/** Check if the configuration file exists */\nexport function configExists(): boolean {\n return existsSync(TITAN_CONFIG_PATH);\n}\n\n/**\n * Check if at least one usable AI provider is configured.\n *\n * \"Usable\" means one of:\n * - Any cloud provider has a non-empty `apiKey` set in config\n * - Any *_API_KEY env var is set (Anthropic, OpenAI, Google, Groq, etc.)\n * - Ollama is reachable at the configured baseUrl (returns at least one model)\n *\n * Used by the gateway boot guard and CLI to refuse to start with empty config\n * instead of letting the user hit \"Internal Server Error\" later.\n *\n * Note: Ollama check is async and is the slowest part of this function (~3s timeout).\n * Callers should `await` and only call once at boot.\n */\nexport async function hasUsableProvider(): Promise<{ ok: boolean; details: string }> {\n const config = loadConfig();\n\n // 1. Check config-file API keys (cloud providers)\n const providers = (config.providers as Record<string, unknown> | undefined) || {};\n const cloudProviderNames = [\n 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',\n 'fireworks', 'xai', 'together', 'deepseek', 'cerebras', 'cohere',\n 'perplexity', 'venice', 'bedrock', 'litellm', 'azure', 'deepinfra',\n 'sambanova', 'kimi', 'huggingface', 'ai21', 'cohere-v2', 'reka',\n 'zhipu', 'yi', 'inflection', 'novita', 'replicate', 'lepton',\n 'anyscale', 'octo', 'nous', 'minimax', 'nvidia',\n ];\n for (const name of cloudProviderNames) {\n const p = providers[name] as { apiKey?: string; enabled?: boolean } | undefined;\n if (p?.enabled === false) continue;\n if (p?.apiKey && p.apiKey.trim().length > 0) {\n return { ok: true, details: `${name} has an API key configured` };\n }\n }\n\n // 2. Check env-var API keys (in case config wasn't reloaded after env var change)\n const envKeys = [\n 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY',\n 'MISTRAL_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'XAI_API_KEY',\n 'TOGETHER_API_KEY', 'DEEPSEEK_API_KEY', 'CEREBRAS_API_KEY', 'COHERE_API_KEY',\n 'PERPLEXITY_API_KEY', 'AZURE_OPENAI_API_KEY',\n ];\n for (const key of envKeys) {\n if (key === 'ANTHROPIC_API_KEY') {\n const anthropic = providers.anthropic as { enabled?: boolean } | undefined;\n if (anthropic?.enabled === false) continue;\n }\n if (process.env[key] && process.env[key]!.trim().length > 0) {\n return { ok: true, details: `${key} is set in environment` };\n }\n }\n\n // 3. Check Ollama reachability (last resort — slow)\n const ollamaUrl = normalizeOllamaBaseUrl((providers.ollama as { baseUrl?: string } | undefined)?.baseUrl\n || process.env.OLLAMA_BASE_URL\n || process.env.OLLAMA_HOST\n || 'http://localhost:11434');\n try {\n const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const json = await res.json() as { models?: { name: string }[] };\n const count = (json.models || []).length;\n if (count > 0) {\n return { ok: true, details: `Ollama at ${ollamaUrl} is reachable (${count} models)` };\n }\n return { ok: false, details: `Ollama at ${ollamaUrl} is reachable but has 0 models — run \"ollama pull qwen3.5:4b\"` };\n }\n } catch {\n // Ollama unreachable, fall through to \"no providers\"\n }\n\n return { ok: false, details: 'No API keys configured and Ollama is not running' };\n}\n\n/** Apply environment variable overrides to raw config */\nfunction applyEnvOverrides(config: Record<string, unknown>): void {\n const envMap: Record<string, (val: string) => void> = {\n TITAN_MODEL: (val) => setNested(config, 'agent.model', val),\n TITAN_GATEWAY_PORT: (val) => setNested(config, 'gateway.port', parseInt(val, 10)),\n TITAN_GATEWAY_HOST: (val) => setNested(config, 'gateway.host', val),\n TITAN_LOG_LEVEL: (val) => setNested(config, 'logging.level', val),\n ANTHROPIC_API_KEY: (val) => setNested(config, 'providers.anthropic.apiKey', val),\n OPENAI_API_KEY: (val) => setNested(config, 'providers.openai.apiKey', val),\n GOOGLE_API_KEY: (val) => setNested(config, 'providers.google.apiKey', val),\n OLLAMA_HOST: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n OLLAMA_BASE_URL: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n DISCORD_TOKEN: (val) => setNested(config, 'channels.discord.token', val),\n TELEGRAM_TOKEN: (val) => setNested(config, 'channels.telegram.token', val),\n SLACK_TOKEN: (val) => setNested(config, 'channels.slack.token', val),\n GOOGLE_OAUTH_CLIENT_ID: (val) => setNested(config, 'oauth.google.clientId', val),\n GOOGLE_OAUTH_CLIENT_SECRET: (val) => setNested(config, 'oauth.google.clientSecret', val),\n OPENROUTER_API_KEY: (val) => setNested(config, 'providers.openrouter.apiKey', val),\n };\n\n // Cloud mode: auto-configure OpenRouter to point at SaaS gateway\n if (process.env.TITAN_CLOUD_MODE === 'true' && process.env.TITAN_CLOUD_API) {\n const cloudApi = process.env.TITAN_CLOUD_API;\n setNested(config, 'providers.openrouter.baseUrl', cloudApi + '/api/v1');\n logger.debug(COMPONENT, `Cloud mode: OpenRouter base URL set to ${cloudApi}/api/v1`);\n }\n\n for (const [envKey, setter] of Object.entries(envMap)) {\n const val = process.env[envKey];\n if (val) {\n if (envKey === 'ANTHROPIC_API_KEY' && isProviderDisabled(config, 'anthropic')) {\n logger.debug(COMPONENT, 'Skipped env override: ANTHROPIC_API_KEY because providers.anthropic.enabled=false');\n continue;\n }\n setter(val);\n logger.debug(COMPONENT, `Applied env override: ${envKey}`);\n }\n }\n}\n\nfunction isProviderDisabled(config: Record<string, unknown>, providerName: string): boolean {\n const providers = config.providers as Record<string, unknown> | undefined;\n const provider = providers?.[providerName] as { enabled?: unknown } | undefined;\n return provider?.enabled === false;\n}\n\nfunction normalizeOllamaBaseUrl(value: string): string {\n let raw = value.trim().replace(/\\/+$/, '');\n if (!/^https?:\\/\\//i.test(raw)) {\n raw = `http://${raw}`;\n }\n\n try {\n const url = new URL(raw);\n if (url.hostname === '0.0.0.0') {\n url.hostname = 'localhost';\n }\n return url.toString().replace(/\\/+$/, '');\n } catch {\n return raw;\n }\n}\n\n/**\n * Recursively diff `raw` against the Zod-parsed `parsed` and return the set\n * of dot-notation paths that exist in raw but were stripped by the schema.\n *\n * Behaviour:\n * - Only walks plain objects (Records). Arrays and primitives are leaf\n * comparisons — if the raw value is an object at a key the schema parsed\n * as something else, we treat the whole subtree as dropped at that path.\n * - Recurses into objects that survive parsing so we catch keys deep inside\n * untyped/permissive subtrees (e.g. `providers.<name>.unknownField`).\n * - Cap recursion depth to defend against pathological self-referential\n * configs; 8 levels covers every legitimate path in TitanConfigSchema.\n */\nfunction findDroppedKeys(\n raw: unknown,\n parsed: unknown,\n pathPrefix: string,\n depth: number = 0,\n): string[] {\n if (depth > 8) return [];\n if (!isPlainObject(raw)) return [];\n const dropped: string[] = [];\n for (const key of Object.keys(raw)) {\n const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const rawVal = raw[key];\n const parsedVal = isPlainObject(parsed) ? (parsed as Record<string, unknown>)[key] : undefined;\n\n if (parsedVal === undefined) {\n // Skip explicit `undefined` in raw (treats `key: undefined` as not-set\n // rather than a dropped key — JSON config files can't express this\n // anyway, but in-memory configs sometimes do via env merging).\n if (rawVal !== undefined) dropped.push(fullPath);\n continue;\n }\n\n // Recurse into matching subtrees so nested unknown keys are also surfaced.\n if (isPlainObject(rawVal) && isPlainObject(parsedVal)) {\n dropped.push(...findDroppedKeys(rawVal, parsedVal, fullPath, depth + 1));\n }\n }\n return dropped;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Set a nested property by dot-notation path */\nfunction setNested(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {\n current[parts[i]] = {};\n }\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n"],"mappings":";AAIA,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB,kBAAkB;AAC9C,SAAS,cAAc,eAAe,kBAAkB,iBAAiB;AACzE,SAAS,yBAA2C;AACpD,OAAO,YAAY;AAEnB,MAAM,YAAY;AAElB,IAAI,eAAmC;AAGhC,SAAS,mBAAgC;AAC5C,SAAO,kBAAkB,MAAM,CAAC,CAAC;AACrC;AAGO,SAAS,aAA0B;AACtC,MAAI,aAAc,QAAO;AAEzB,mBAAiB,UAAU;AAE3B,MAAI,YAAqC,CAAC;AAE1C,MAAI,WAAW,iBAAiB,GAAG;AAC/B,UAAM,SAAS,aAAsC,iBAAiB;AACtE,QAAI,QAAQ;AACR,kBAAY;AACZ,aAAO,MAAM,WAAW,sBAAsB,iBAAiB,EAAE;AAAA,IACrE,OAAO;AACH,aAAO,KAAK,WAAW,6BAA6B,iBAAiB,kBAAkB;AAAA,IAC3F;AAAA,EACJ,OAAO;AACH,WAAO,KAAK,WAAW,sCAAsC;AAAA,EACjE;AAGA,oBAAkB,SAAS;AAM3B,MAAI,aAAa,OAAO,cAAc,YAAY,UAAU,WAAW;AACnE,UAAM,MAAM;AACZ,UAAM,UAAU,IAAI;AACpB,QAAI,WAAW,OAAO,YAAY,UAAU;AACxC,YAAM,UAAW,IAAI,WAAmD,CAAC;AACzE,YAAM,cAAe,QAAQ,QAAgD,CAAC;AAG9E,UAAI,UAAU,EAAE,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,YAAY,EAAE;AACjE,aAAO,IAAI;AACX,aAAO,KAAK,WAAW,uHAAkH;AAAA,IAC7I;AAAA,EACJ;AAWA,MAAI;AACA,UAAM,SAAS,kBAAkB,UAAU,SAAS;AACpD,QAAI,OAAO,SAAS;AAChB,YAAM,eAAe,gBAAgB,WAAW,OAAO,MAAiC,EAAE;AAC1F,iBAAW,QAAQ,cAAc;AAC7B,eAAO;AAAA,UACH;AAAA,UACA,uBAAuB,IAAI;AAAA,QAE/B;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ,QAAQ;AAAA,EAER;AAMA,QAAM,SAAS,kBAAkB,UAAU,SAAS;AACpD,MAAI,OAAO,SAAS;AAChB,mBAAe,OAAO;AAAA,EAC1B,OAAO;AACH,UAAM,SAAS,OAAO,MAAM,OAAO,IAAI,OAAK,GAAG,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC1F,WAAO,KAAK,WAAW,6BAA6B,MAAM,6CAAwC;AAElG,UAAM,WAAW,iBAAiB;AAClC,UAAM,SAAS,UAAU,UAAqC,SAAS;AAEvE,UAAM,UAAU,kBAAkB,UAAU,MAAM;AAClD,QAAI,QAAQ,SAAS;AACjB,qBAAe,QAAQ;AAAA,IAC3B,OAAO;AACH,aAAO,MAAM,WAAW,wFAAmF;AAC3G,qBAAe;AAAA,IACnB;AAAA,EACJ;AAEA,SAAO;AACX;AAGO,SAAS,WAAW,QAA2B;AAClD,mBAAiB,UAAU;AAC3B,gBAAc,mBAAmB,MAAM;AACvC,iBAAe;AACf,SAAO,KAAK,WAAW,mBAAmB,iBAAiB,EAAE;AACjE;AAGO,SAAS,aAAa,SAA4C;AACrE,QAAM,UAAU,WAAW;AAC3B,QAAM,UAAU,UAAU,SAAoC,OAAkC;AAChG,QAAM,YAAY,kBAAkB,MAAM,OAAO;AACjD,aAAW,SAAS;AACpB,SAAO;AACX;AAGO,SAAS,mBAAyB;AACrC,iBAAe;AACnB;AAGO,SAAS,eAAwB;AACpC,SAAO,WAAW,iBAAiB;AACvC;AAgBA,eAAsB,oBAA+D;AACjF,QAAM,SAAS,WAAW;AAG1B,QAAM,YAAa,OAAO,aAAqD,CAAC;AAChF,QAAM,qBAAqB;AAAA,IACvB;AAAA,IAAa;AAAA,IAAU;AAAA,IAAU;AAAA,IAAQ;AAAA,IAAW;AAAA,IACpD;AAAA,IAAa;AAAA,IAAO;AAAA,IAAY;AAAA,IAAY;AAAA,IAAY;AAAA,IACxD;AAAA,IAAc;AAAA,IAAU;AAAA,IAAW;AAAA,IAAW;AAAA,IAAS;AAAA,IACvD;AAAA,IAAa;AAAA,IAAQ;AAAA,IAAe;AAAA,IAAQ;AAAA,IAAa;AAAA,IACzD;AAAA,IAAS;AAAA,IAAM;AAAA,IAAc;AAAA,IAAU;AAAA,IAAa;AAAA,IACpD;AAAA,IAAY;AAAA,IAAQ;AAAA,IAAQ;AAAA,IAAW;AAAA,EAC3C;AACA,aAAW,QAAQ,oBAAoB;AACnC,UAAM,IAAI,UAAU,IAAI;AACxB,QAAI,GAAG,YAAY,MAAO;AAC1B,QAAI,GAAG,UAAU,EAAE,OAAO,KAAK,EAAE,SAAS,GAAG;AACzC,aAAO,EAAE,IAAI,MAAM,SAAS,GAAG,IAAI,6BAA6B;AAAA,IACpE;AAAA,EACJ;AAGA,QAAM,UAAU;AAAA,IACZ;AAAA,IAAqB;AAAA,IAAkB;AAAA,IAAkB;AAAA,IACzD;AAAA,IAAmB;AAAA,IAAsB;AAAA,IAAqB;AAAA,IAC9D;AAAA,IAAoB;AAAA,IAAoB;AAAA,IAAoB;AAAA,IAC5D;AAAA,IAAsB;AAAA,EAC1B;AACA,aAAW,OAAO,SAAS;AACvB,QAAI,QAAQ,qBAAqB;AAC7B,YAAM,YAAY,UAAU;AAC5B,UAAI,WAAW,YAAY,MAAO;AAAA,IACtC;AACA,QAAI,QAAQ,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,EAAG,KAAK,EAAE,SAAS,GAAG;AACzD,aAAO,EAAE,IAAI,MAAM,SAAS,GAAG,GAAG,yBAAyB;AAAA,IAC/D;AAAA,EACJ;AAGA,QAAM,YAAY,uBAAwB,UAAU,QAA6C,WAC1F,QAAQ,IAAI,mBACZ,QAAQ,IAAI,eACZ,wBAAwB;AAC/B,MAAI;AACA,UAAM,MAAM,MAAM,MAAM,GAAG,SAAS,aAAa,EAAE,QAAQ,YAAY,QAAQ,GAAI,EAAE,CAAC;AACtF,QAAI,IAAI,IAAI;AACR,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,YAAM,SAAS,KAAK,UAAU,CAAC,GAAG;AAClC,UAAI,QAAQ,GAAG;AACX,eAAO,EAAE,IAAI,MAAM,SAAS,aAAa,SAAS,kBAAkB,KAAK,WAAW;AAAA,MACxF;AACA,aAAO,EAAE,IAAI,OAAO,SAAS,aAAa,SAAS,qEAAgE;AAAA,IACvH;AAAA,EACJ,QAAQ;AAAA,EAER;AAEA,SAAO,EAAE,IAAI,OAAO,SAAS,mDAAmD;AACpF;AAGA,SAAS,kBAAkB,QAAuC;AAC9D,QAAM,SAAgD;AAAA,IAClD,aAAa,CAAC,QAAQ,UAAU,QAAQ,eAAe,GAAG;AAAA,IAC1D,oBAAoB,CAAC,QAAQ,UAAU,QAAQ,gBAAgB,SAAS,KAAK,EAAE,CAAC;AAAA,IAChF,oBAAoB,CAAC,QAAQ,UAAU,QAAQ,gBAAgB,GAAG;AAAA,IAClE,iBAAiB,CAAC,QAAQ,UAAU,QAAQ,iBAAiB,GAAG;AAAA,IAChE,mBAAmB,CAAC,QAAQ,UAAU,QAAQ,8BAA8B,GAAG;AAAA,IAC/E,gBAAgB,CAAC,QAAQ,UAAU,QAAQ,2BAA2B,GAAG;AAAA,IACzE,gBAAgB,CAAC,QAAQ,UAAU,QAAQ,2BAA2B,GAAG;AAAA,IACzE,aAAa,CAAC,QAAQ,UAAU,QAAQ,4BAA4B,uBAAuB,GAAG,CAAC;AAAA,IAC/F,iBAAiB,CAAC,QAAQ,UAAU,QAAQ,4BAA4B,uBAAuB,GAAG,CAAC;AAAA,IACnG,eAAe,CAAC,QAAQ,UAAU,QAAQ,0BAA0B,GAAG;AAAA,IACvE,gBAAgB,CAAC,QAAQ,UAAU,QAAQ,2BAA2B,GAAG;AAAA,IACzE,aAAa,CAAC,QAAQ,UAAU,QAAQ,wBAAwB,GAAG;AAAA,IACnE,wBAAwB,CAAC,QAAQ,UAAU,QAAQ,yBAAyB,GAAG;AAAA,IAC/E,4BAA4B,CAAC,QAAQ,UAAU,QAAQ,6BAA6B,GAAG;AAAA,IACvF,oBAAoB,CAAC,QAAQ,UAAU,QAAQ,+BAA+B,GAAG;AAAA,EACrF;AAGA,MAAI,QAAQ,IAAI,qBAAqB,UAAU,QAAQ,IAAI,iBAAiB;AACxE,UAAM,WAAW,QAAQ,IAAI;AAC7B,cAAU,QAAQ,gCAAgC,WAAW,SAAS;AACtE,WAAO,MAAM,WAAW,0CAA0C,QAAQ,SAAS;AAAA,EACvF;AAEA,aAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,MAAM,QAAQ,IAAI,MAAM;AAC9B,QAAI,KAAK;AACL,UAAI,WAAW,uBAAuB,mBAAmB,QAAQ,WAAW,GAAG;AAC3E,eAAO,MAAM,WAAW,mFAAmF;AAC3G;AAAA,MACJ;AACA,aAAO,GAAG;AACV,aAAO,MAAM,WAAW,yBAAyB,MAAM,EAAE;AAAA,IAC7D;AAAA,EACJ;AACJ;AAEA,SAAS,mBAAmB,QAAiC,cAA+B;AACxF,QAAM,YAAY,OAAO;AACzB,QAAM,WAAW,YAAY,YAAY;AACzC,SAAO,UAAU,YAAY;AACjC;AAEA,SAAS,uBAAuB,OAAuB;AACnD,MAAI,MAAM,MAAM,KAAK,EAAE,QAAQ,QAAQ,EAAE;AACzC,MAAI,CAAC,gBAAgB,KAAK,GAAG,GAAG;AAC5B,UAAM,UAAU,GAAG;AAAA,EACvB;AAEA,MAAI;AACA,UAAM,MAAM,IAAI,IAAI,GAAG;AACvB,QAAI,IAAI,aAAa,WAAW;AAC5B,UAAI,WAAW;AAAA,IACnB;AACA,WAAO,IAAI,SAAS,EAAE,QAAQ,QAAQ,EAAE;AAAA,EAC5C,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAeA,SAAS,gBACL,KACA,QACA,YACA,QAAgB,GACR;AACR,MAAI,QAAQ,EAAG,QAAO,CAAC;AACvB,MAAI,CAAC,cAAc,GAAG,EAAG,QAAO,CAAC;AACjC,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,OAAO,KAAK,GAAG,GAAG;AAChC,UAAM,WAAW,aAAa,GAAG,UAAU,IAAI,GAAG,KAAK;AACvD,UAAM,SAAS,IAAI,GAAG;AACtB,UAAM,YAAY,cAAc,MAAM,IAAK,OAAmC,GAAG,IAAI;AAErF,QAAI,cAAc,QAAW;AAIzB,UAAI,WAAW,OAAW,SAAQ,KAAK,QAAQ;AAC/C;AAAA,IACJ;AAGA,QAAI,cAAc,MAAM,KAAK,cAAc,SAAS,GAAG;AACnD,cAAQ,KAAK,GAAG,gBAAgB,QAAQ,WAAW,UAAU,QAAQ,CAAC,CAAC;AAAA,IAC3E;AAAA,EACJ;AACA,SAAO;AACX;AAEA,SAAS,cAAc,OAAkD;AACrE,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC9E;AAGA,SAAS,UAAU,KAA8B,MAAc,OAAsB;AACjF,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,UAAmC;AACvC,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACvC,QAAI,CAAC,QAAQ,MAAM,CAAC,CAAC,KAAK,OAAO,QAAQ,MAAM,CAAC,CAAC,MAAM,UAAU;AAC7D,cAAQ,MAAM,CAAC,CAAC,IAAI,CAAC;AAAA,IACzB;AACA,cAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,EAC9B;AACA,UAAQ,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;AACvC;","names":[]}
1
+ {"version":3,"sources":["../../src/config/config.ts"],"sourcesContent":["/**\n * TITAN Configuration Manager\n * Loads, validates, and persists configuration from ~/.titan/titan.json\n */\nimport { existsSync } from 'fs';\nimport { TITAN_CONFIG_PATH, TITAN_HOME } from '../utils/constants.js';\nimport { readJsonFile, writeJsonFile, mkdirIfNotExists, deepMerge } from '../utils/helpers.js';\nimport { TitanConfigSchema, type TitanConfig } from './schema.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Config';\n\nlet cachedConfig: TitanConfig | null = null;\n\n/** Get the default configuration */\nexport function getDefaultConfig(): TitanConfig {\n return TitanConfigSchema.parse({});\n}\n\n/** Load configuration from disk, merging with defaults */\nexport function loadConfig(): TitanConfig {\n if (cachedConfig) return cachedConfig;\n\n mkdirIfNotExists(TITAN_HOME);\n\n let rawConfig: Record<string, unknown> = {};\n\n if (existsSync(TITAN_CONFIG_PATH)) {\n const loaded = readJsonFile<Record<string, unknown>>(TITAN_CONFIG_PATH);\n if (loaded) {\n rawConfig = loaded;\n logger.debug(COMPONENT, `Loaded config from ${TITAN_CONFIG_PATH}`);\n } else {\n logger.warn(COMPONENT, `Failed to parse config at ${TITAN_CONFIG_PATH}, using defaults`);\n }\n } else {\n logger.info(COMPONENT, 'No config file found, using defaults');\n }\n\n // Apply environment variables\n applyEnvOverrides(rawConfig);\n\n // v4.8.4: migrate a top-level `auth` block to `gateway.auth`. The\n // documented path has always been `gateway.auth`, but users (and\n // Claude) naturally try `auth` at the root. Rather than strip it\n // silently and warn, move it to the canonical location and continue.\n if (rawConfig && typeof rawConfig === 'object' && 'auth' in rawConfig) {\n const raw = rawConfig as Record<string, unknown>;\n const topAuth = raw.auth as Record<string, unknown> | undefined;\n if (topAuth && typeof topAuth === 'object') {\n const gateway = (raw.gateway as Record<string, unknown> | undefined) ?? {};\n const gatewayAuth = (gateway.auth as Record<string, unknown> | undefined) ?? {};\n // gateway.auth wins if both are set — explicit nested wins\n // over migrated top-level.\n raw.gateway = { ...gateway, auth: { ...topAuth, ...gatewayAuth } };\n delete raw.auth;\n logger.info(COMPONENT, 'Migrated top-level `auth` → `gateway.auth`. Update titan.json to nest it under `gateway` to silence this notice.');\n }\n }\n\n // v7.0: migrate organism.pressureThreshold 1.2 → 0.4. The old default was\n // mathematically unreachable (a fully starved drive maxes ~0.49), so Soma\n // could never propose work — and Zod materialized that dead value into\n // every saved titan.json. Nobody chose 1.2 deliberately (it was the\n // default), so an exact match is safe to retune; any other value is an\n // explicit user choice and is respected.\n {\n const organism = (rawConfig as Record<string, unknown>).organism as Record<string, unknown> | undefined;\n if (organism && typeof organism === 'object' && organism.pressureThreshold === 1.2) {\n organism.pressureThreshold = 0.4;\n logger.info(COMPONENT, 'Migrated organism.pressureThreshold 1.2 → 0.4 (the old default could never fire a proposal). Set any other value in titan.json to override.');\n }\n }\n\n // Detect unknown keys at every nesting depth BEFORE Zod silently strips\n // them. Hunt Finding #1 (2026-04-14) covered the top-level case\n // (`facebook: {...}` ignored). This expanded check also catches nested\n // typos like `providers.anthropic.unknownField` so users learn about\n // dropped keys immediately rather than chasing \"why doesn't my setting\n // do anything\" days later.\n //\n // The diff is purely informational — invalid keys never block startup\n // (we still feed `rawConfig` through Zod's permissive parse).\n try {\n const parsed = TitanConfigSchema.safeParse(rawConfig);\n if (parsed.success) {\n const droppedPaths = findDroppedKeys(rawConfig, parsed.data as Record<string, unknown>, '');\n for (const path of droppedPaths) {\n logger.warn(\n COMPONENT,\n `Unknown config key: ${path}. Will be ignored. ` +\n `If intentional, extend TitanConfigSchema in src/config/schema.ts.`,\n );\n }\n }\n } catch {\n // Introspection failed — never let a diagnostic warning block load.\n }\n\n // Validate and merge with defaults via Zod.\n // CRITICAL: On validation failure, deep-merge raw config over defaults\n // so that valid user settings (daemon.enabled, autonomy.mode, etc.) survive.\n // Previously this fell back to pure defaults, wiping ALL user config on any error.\n const result = TitanConfigSchema.safeParse(rawConfig);\n if (result.success) {\n cachedConfig = result.data;\n } else {\n const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');\n logger.warn(COMPONENT, `Config validation issues (${issues}) — merging valid fields over defaults`);\n // Deep-merge raw config over defaults so valid sections survive\n const defaults = getDefaultConfig();\n const merged = deepMerge(defaults as Record<string, unknown>, rawConfig) as TitanConfig;\n // Try parsing the merged result — if it still fails, use defaults but log loudly\n const reparse = TitanConfigSchema.safeParse(merged);\n if (reparse.success) {\n cachedConfig = reparse.data;\n } else {\n logger.error(COMPONENT, `Config still invalid after merge — falling back to defaults. Fix your titan.json.`);\n cachedConfig = defaults;\n }\n }\n\n return cachedConfig;\n}\n\n/** Save current configuration to disk */\nexport function saveConfig(config: TitanConfig): void {\n mkdirIfNotExists(TITAN_HOME);\n writeJsonFile(TITAN_CONFIG_PATH, config);\n cachedConfig = config;\n logger.info(COMPONENT, `Config saved to ${TITAN_CONFIG_PATH}`);\n}\n\n/** Update specific fields in the config */\nexport function updateConfig(partial: Partial<TitanConfig>): TitanConfig {\n const current = loadConfig();\n const updated = deepMerge(current as Record<string, unknown>, partial as Record<string, unknown>) as TitanConfig;\n const validated = TitanConfigSchema.parse(updated);\n saveConfig(validated);\n return validated;\n}\n\n/** Reset config cache (useful for testing) */\nexport function resetConfigCache(): void {\n cachedConfig = null;\n}\n\n/** Check if the configuration file exists */\nexport function configExists(): boolean {\n return existsSync(TITAN_CONFIG_PATH);\n}\n\n/**\n * Check if at least one usable AI provider is configured.\n *\n * \"Usable\" means one of:\n * - Any cloud provider has a non-empty `apiKey` set in config\n * - Any *_API_KEY env var is set (Anthropic, OpenAI, Google, Groq, etc.)\n * - Ollama is reachable at the configured baseUrl (returns at least one model)\n *\n * Used by the gateway boot guard and CLI to refuse to start with empty config\n * instead of letting the user hit \"Internal Server Error\" later.\n *\n * Note: Ollama check is async and is the slowest part of this function (~3s timeout).\n * Callers should `await` and only call once at boot.\n */\nexport async function hasUsableProvider(): Promise<{ ok: boolean; details: string }> {\n const config = loadConfig();\n\n // 1. Check config-file API keys (cloud providers)\n const providers = (config.providers as Record<string, unknown> | undefined) || {};\n const cloudProviderNames = [\n 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',\n 'fireworks', 'xai', 'together', 'deepseek', 'cerebras', 'cohere',\n 'perplexity', 'venice', 'bedrock', 'litellm', 'azure', 'deepinfra',\n 'sambanova', 'kimi', 'huggingface', 'ai21', 'cohere-v2', 'reka',\n 'zhipu', 'yi', 'inflection', 'novita', 'replicate', 'lepton',\n 'anyscale', 'octo', 'nous', 'minimax', 'nvidia',\n ];\n for (const name of cloudProviderNames) {\n const p = providers[name] as { apiKey?: string; enabled?: boolean } | undefined;\n if (p?.enabled === false) continue;\n if (p?.apiKey && p.apiKey.trim().length > 0) {\n return { ok: true, details: `${name} has an API key configured` };\n }\n }\n\n // 2. Check env-var API keys (in case config wasn't reloaded after env var change)\n const envKeys = [\n 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GROQ_API_KEY',\n 'MISTRAL_API_KEY', 'OPENROUTER_API_KEY', 'FIREWORKS_API_KEY', 'XAI_API_KEY',\n 'TOGETHER_API_KEY', 'DEEPSEEK_API_KEY', 'CEREBRAS_API_KEY', 'COHERE_API_KEY',\n 'PERPLEXITY_API_KEY', 'AZURE_OPENAI_API_KEY',\n ];\n for (const key of envKeys) {\n if (key === 'ANTHROPIC_API_KEY') {\n const anthropic = providers.anthropic as { enabled?: boolean } | undefined;\n if (anthropic?.enabled === false) continue;\n }\n if (process.env[key] && process.env[key]!.trim().length > 0) {\n return { ok: true, details: `${key} is set in environment` };\n }\n }\n\n // 3. Check Ollama reachability (last resort — slow)\n const ollamaUrl = normalizeOllamaBaseUrl((providers.ollama as { baseUrl?: string } | undefined)?.baseUrl\n || process.env.OLLAMA_BASE_URL\n || process.env.OLLAMA_HOST\n || 'http://localhost:11434');\n try {\n const res = await fetch(`${ollamaUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });\n if (res.ok) {\n const json = await res.json() as { models?: { name: string }[] };\n const count = (json.models || []).length;\n if (count > 0) {\n return { ok: true, details: `Ollama at ${ollamaUrl} is reachable (${count} models)` };\n }\n return { ok: false, details: `Ollama at ${ollamaUrl} is reachable but has 0 models — run \"ollama pull qwen3.5:4b\"` };\n }\n } catch {\n // Ollama unreachable, fall through to \"no providers\"\n }\n\n return { ok: false, details: 'No API keys configured and Ollama is not running' };\n}\n\n/** Apply environment variable overrides to raw config */\nfunction applyEnvOverrides(config: Record<string, unknown>): void {\n const envMap: Record<string, (val: string) => void> = {\n TITAN_MODEL: (val) => setNested(config, 'agent.model', val),\n TITAN_GATEWAY_PORT: (val) => setNested(config, 'gateway.port', parseInt(val, 10)),\n TITAN_GATEWAY_HOST: (val) => setNested(config, 'gateway.host', val),\n TITAN_LOG_LEVEL: (val) => setNested(config, 'logging.level', val),\n ANTHROPIC_API_KEY: (val) => setNested(config, 'providers.anthropic.apiKey', val),\n OPENAI_API_KEY: (val) => setNested(config, 'providers.openai.apiKey', val),\n GOOGLE_API_KEY: (val) => setNested(config, 'providers.google.apiKey', val),\n OLLAMA_HOST: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n OLLAMA_BASE_URL: (val) => setNested(config, 'providers.ollama.baseUrl', normalizeOllamaBaseUrl(val)),\n DISCORD_TOKEN: (val) => setNested(config, 'channels.discord.token', val),\n TELEGRAM_TOKEN: (val) => setNested(config, 'channels.telegram.token', val),\n SLACK_TOKEN: (val) => setNested(config, 'channels.slack.token', val),\n GOOGLE_OAUTH_CLIENT_ID: (val) => setNested(config, 'oauth.google.clientId', val),\n GOOGLE_OAUTH_CLIENT_SECRET: (val) => setNested(config, 'oauth.google.clientSecret', val),\n OPENROUTER_API_KEY: (val) => setNested(config, 'providers.openrouter.apiKey', val),\n };\n\n // Cloud mode: auto-configure OpenRouter to point at SaaS gateway\n if (process.env.TITAN_CLOUD_MODE === 'true' && process.env.TITAN_CLOUD_API) {\n const cloudApi = process.env.TITAN_CLOUD_API;\n setNested(config, 'providers.openrouter.baseUrl', cloudApi + '/api/v1');\n logger.debug(COMPONENT, `Cloud mode: OpenRouter base URL set to ${cloudApi}/api/v1`);\n }\n\n for (const [envKey, setter] of Object.entries(envMap)) {\n const val = process.env[envKey];\n if (val) {\n if (envKey === 'ANTHROPIC_API_KEY' && isProviderDisabled(config, 'anthropic')) {\n logger.debug(COMPONENT, 'Skipped env override: ANTHROPIC_API_KEY because providers.anthropic.enabled=false');\n continue;\n }\n setter(val);\n logger.debug(COMPONENT, `Applied env override: ${envKey}`);\n }\n }\n}\n\nfunction isProviderDisabled(config: Record<string, unknown>, providerName: string): boolean {\n const providers = config.providers as Record<string, unknown> | undefined;\n const provider = providers?.[providerName] as { enabled?: unknown } | undefined;\n return provider?.enabled === false;\n}\n\nfunction normalizeOllamaBaseUrl(value: string): string {\n let raw = value.trim().replace(/\\/+$/, '');\n if (!/^https?:\\/\\//i.test(raw)) {\n raw = `http://${raw}`;\n }\n\n try {\n const url = new URL(raw);\n if (url.hostname === '0.0.0.0') {\n url.hostname = 'localhost';\n }\n return url.toString().replace(/\\/+$/, '');\n } catch {\n return raw;\n }\n}\n\n/**\n * Recursively diff `raw` against the Zod-parsed `parsed` and return the set\n * of dot-notation paths that exist in raw but were stripped by the schema.\n *\n * Behaviour:\n * - Only walks plain objects (Records). Arrays and primitives are leaf\n * comparisons — if the raw value is an object at a key the schema parsed\n * as something else, we treat the whole subtree as dropped at that path.\n * - Recurses into objects that survive parsing so we catch keys deep inside\n * untyped/permissive subtrees (e.g. `providers.<name>.unknownField`).\n * - Cap recursion depth to defend against pathological self-referential\n * configs; 8 levels covers every legitimate path in TitanConfigSchema.\n */\nfunction findDroppedKeys(\n raw: unknown,\n parsed: unknown,\n pathPrefix: string,\n depth: number = 0,\n): string[] {\n if (depth > 8) return [];\n if (!isPlainObject(raw)) return [];\n const dropped: string[] = [];\n for (const key of Object.keys(raw)) {\n const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n const rawVal = raw[key];\n const parsedVal = isPlainObject(parsed) ? (parsed as Record<string, unknown>)[key] : undefined;\n\n if (parsedVal === undefined) {\n // Skip explicit `undefined` in raw (treats `key: undefined` as not-set\n // rather than a dropped key — JSON config files can't express this\n // anyway, but in-memory configs sometimes do via env merging).\n if (rawVal !== undefined) dropped.push(fullPath);\n continue;\n }\n\n // Recurse into matching subtrees so nested unknown keys are also surfaced.\n if (isPlainObject(rawVal) && isPlainObject(parsedVal)) {\n dropped.push(...findDroppedKeys(rawVal, parsedVal, fullPath, depth + 1));\n }\n }\n return dropped;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Set a nested property by dot-notation path */\nfunction setNested(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {\n current[parts[i]] = {};\n }\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = value;\n}\n"],"mappings":"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","names":[]}
@@ -51,6 +51,9 @@ const ChannelConfigSchema = z.object({
51
51
  dmPolicy: z.enum(["pairing", "open", "closed"]).default("pairing")
52
52
  });
53
53
  const MessengerChannelConfigSchema = ChannelConfigSchema.extend({
54
+ /** Facebook App Secret. When set (or via FB_APP_SECRET env), inbound webhook
55
+ * POSTs are HMAC-verified against X-Hub-Signature-256 before processing. */
56
+ appSecret: z.string().optional(),
54
57
  voiceReplies: z.object({
55
58
  enabled: z.boolean().default(true),
56
59
  voice: z.string().default("andrew"),
@@ -504,7 +507,11 @@ const CapsolverConfigSchema = z.object({
504
507
  const OrganismConfigSchema = z.object({
505
508
  enabled: z.boolean().default(true).describe("Master switch. When true (default for v5.0+), Soma registers driveTick, writes drive state, and injects the hormonal ambient-state block into the system prompt. Flip via titan.json, Soma widget header, or Settings."),
506
509
  hormonesInPrompt: z.boolean().default(true).describe("Include hormonal ambient-state block in the system prompt when Soma is enabled."),
507
- pressureThreshold: z.number().min(0).max(5).default(1.2).describe("Combined drive pressure above which Soma fires a proposal. Raise to make Soma more conservative."),
510
+ // v7.0 life loop: was 1.2 mathematically unreachable (a fully starved
511
+ // drive maxes near 0.49), so Soma had never once proposed work. 0.4 lets a
512
+ // genuinely neglected drive fire a proposal — which still goes through
513
+ // shadow rehearsal + the approvals inbox, never auto-runs.
514
+ pressureThreshold: z.number().min(0).max(5).default(0.4).describe("Combined drive pressure above which Soma fires a proposal. Raise to make Soma more conservative."),
508
515
  driveSetpoints: z.record(z.string(), z.number().min(0).max(1)).optional().describe("Per-drive setpoint overrides: { purpose: 0.7, hunger: 0.6, ... }"),
509
516
  driveWeights: z.record(z.string(), z.number().min(0.1).max(3)).optional().describe("Per-drive weight overrides for pressure fusion. 1.0 is baseline; higher = more urgent."),
510
517
  disabledDrives: z.array(z.string()).default([]).describe("Drive IDs to skip entirely in computeAllDrives + pressure fusion."),
@@ -568,6 +575,16 @@ const TitanConfigSchema = z.object({
568
575
  })).default({})
569
576
  }).default({}),
570
577
  organism: OrganismConfigSchema.default({}),
578
+ /** v7.0 Muscle Memory — automatic, replay-verified skill learning.
579
+ * Safe to enable by default: proposals never auto-adopt; every learned
580
+ * skill must pass a deterministic replay exam before it is shown. */
581
+ muscleMemory: z.object({
582
+ enabled: z.boolean().default(true),
583
+ /** How many successful repetitions of a workflow before TITAN drafts a skill. */
584
+ minOccurrences: z.number().int().min(2).default(3),
585
+ /** Replay-verify drafts through the eval harness before proposing (the trust layer). */
586
+ verifyBeforePropose: z.boolean().default(true)
587
+ }).default({}),
571
588
  selfMod: SelfModConfigSchema.default({}),
572
589
  homelab: HomelabConfigSchema.default({}),
573
590
  telephony: TelephonyConfigSchema,