onbuzz 4.9.13 → 4.10.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 (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,793 +1,792 @@
1
- /**
2
- * DiscordService — Remote agent interface via Discord Bot
3
- *
4
- * Purpose:
5
- * - Conversational interface with agents from Discord channels
6
- * - Many-to-many mapping: each agent can be on 0..N channels, each channel can have 0..N agents
7
- * - @agent-name mention routing (single-agent channels route directly)
8
- * - Smart response formatting (markdown, code blocks, message splitting)
9
- * - Broadcast interception for real-time agent response relay
10
- *
11
- * Architecture:
12
- * - Uses discord.js v14+ (lazy-imported, optional dependency)
13
- * - Gateway intents: Guilds, GuildMessages, MessageContent (privileged)
14
- * - Intercepts WebSocket broadcasts to capture agent responses
15
- * - Routes user messages to agents via orchestrator.processRequest()
16
- * - Persists config to {userData}/discord/discord-config.json
17
- */
18
-
19
- import { promises as fs } from 'fs';
20
- import path from 'path';
21
- import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
22
- import { INTERFACE_TYPES } from '../utilities/constants.js';
23
- import { filterContentForExternalRelay, resolveBlockTargets } from './channelFilter.js';
24
- import { createDiscordSource } from './messageSource.js';
25
-
26
- const DISCORD_STATUS = {
27
- DISCONNECTED: 'disconnected',
28
- CONNECTING: 'connecting',
29
- CONNECTED: 'connected',
30
- FAILED: 'failed'
31
- };
32
-
33
- const MAX_MESSAGE_LENGTH = 1950; // Discord limit is 2000, leave room for formatting
34
- const INTERACTION_TTL_MS = 30 * 60 * 1000; // 30 minutes — only relay to channels with recent interaction
35
-
36
- class DiscordService {
37
- constructor(logger = null) {
38
- this.logger = logger;
39
-
40
- // Dependencies (set via setters)
41
- this.orchestrator = null;
42
- this.agentPool = null;
43
- this.webSocketManager = null;
44
- this.flowExecutor = null;
45
-
46
- // Bot state
47
- this.client = null;
48
- this.status = DISCORD_STATUS.DISCONNECTED;
49
-
50
- // Channel-agent mappings: channelKey -> [agentId]
51
- // channelKey format: "guildId:channelId" or "guildId:threadId"
52
- // Both channels and threads use the same mapping structure.
53
- // When a message arrives in a thread, we check the thread key first,
54
- // then fall back to the parent channel key.
55
- this.channelMappings = {};
56
-
57
- // Track recent interactions: "agentId:routingKey" -> { channelKey, timestamp }
58
- // Used to determine where to relay agent responses
59
- this.recentInteractions = new Map();
60
-
61
- // Sticky agent per routing key: routingKey -> lastAgentId
62
- this.stickyAgent = new Map();
63
-
64
- // Cached Discord metadata for UI display
65
- this.knownGuilds = {};
66
- this.knownChannels = {};
67
-
68
- // Config persistence
69
- this.dataDir = null;
70
- this.configPath = null;
71
- this.config = {};
72
-
73
- // Original broadcast (saved before wrapping)
74
- this._originalBroadcast = null;
75
- }
76
-
77
- // --- Dependency Injection ---
78
-
79
- setOrchestrator(orchestrator) { this.orchestrator = orchestrator; }
80
- setAgentPool(agentPool) { this.agentPool = agentPool; }
81
- setWebSocketManager(wsManager) {
82
- this.webSocketManager = wsManager;
83
- this._interceptBroadcasts(wsManager);
84
- }
85
- setFlowExecutor(flowExecutor) { this.flowExecutor = flowExecutor; }
86
-
87
- // --- Config Persistence ---
88
-
89
- async _ensureDataDir() {
90
- if (!this.dataDir) {
91
- await ensureUserDataDirs();
92
- const paths = getUserDataPaths();
93
- this.dataDir = path.join(paths.base, 'discord');
94
- this.configPath = path.join(this.dataDir, 'discord-config.json');
95
- await fs.mkdir(this.dataDir, { recursive: true });
96
- }
97
- }
98
-
99
- async _loadConfig() {
100
- await this._ensureDataDir();
101
- try {
102
- const data = await fs.readFile(this.configPath, 'utf8');
103
- this.config = JSON.parse(data);
104
- this.channelMappings = this.config.channelMappings || {};
105
- this.knownGuilds = this.config.knownGuilds || {};
106
- this.knownChannels = this.config.knownChannels || {};
107
- } catch {
108
- this.config = {};
109
- }
110
- }
111
-
112
- async _saveConfig() {
113
- await this._ensureDataDir();
114
- this.config.channelMappings = this.channelMappings;
115
- this.config.knownGuilds = this.knownGuilds;
116
- this.config.knownChannels = this.knownChannels;
117
- this.config.updatedAt = new Date().toISOString();
118
- await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
119
- }
120
-
121
- // --- Lifecycle ---
122
-
123
- async autoConnect() {
124
- await this._loadConfig();
125
- if (this.config.botToken) {
126
- try {
127
- await this.connect(this.config.botToken);
128
- } catch (error) {
129
- this.logger?.warn('[DiscordService] Auto-connect failed', { error: error.message });
130
- }
131
- }
132
- }
133
-
134
- async connect(botToken) {
135
- if (this.status === DISCORD_STATUS.CONNECTED) {
136
- await this.disconnect();
137
- }
138
-
139
- this.status = DISCORD_STATUS.CONNECTING;
140
- this.logger?.info('[DiscordService] Connecting...');
141
-
142
- try {
143
- // Lazy import discord.js (optional dependency)
144
- const { Client, GatewayIntentBits, ChannelType } = await import('discord.js');
145
- this._ChannelType = ChannelType;
146
-
147
- this.client = new Client({
148
- intents: [
149
- GatewayIntentBits.Guilds,
150
- GatewayIntentBits.GuildMessages,
151
- GatewayIntentBits.MessageContent
152
- ]
153
- });
154
-
155
- // Wait for ready event
156
- await new Promise((resolve, reject) => {
157
- const timeout = setTimeout(() => reject(new Error('Discord login timed out after 30s')), 30000);
158
-
159
- this.client.once('ready', () => {
160
- clearTimeout(timeout);
161
- resolve();
162
- });
163
-
164
- this.client.once('error', (err) => {
165
- clearTimeout(timeout);
166
- reject(err);
167
- });
168
-
169
- this.client.login(botToken).catch(reject);
170
- });
171
-
172
- const botUser = this.client.user;
173
- this.logger?.info('[DiscordService] Connected', {
174
- botName: botUser.username,
175
- guildCount: this.client.guilds.cache.size
176
- });
177
-
178
- // Save config
179
- this.config.botToken = botToken;
180
- this.config.botUsername = botUser.username;
181
- this.config.botId = botUser.id;
182
- await this._saveConfig();
183
-
184
- // Cache guild/channel metadata
185
- this._cacheGuildMetadata();
186
-
187
- // Setup event handlers
188
- this._setupHandlers();
189
-
190
- this.status = DISCORD_STATUS.CONNECTED;
191
-
192
- return { username: botUser.username, id: botUser.id };
193
- } catch (error) {
194
- this.status = DISCORD_STATUS.FAILED;
195
- this.logger?.error('[DiscordService] Connection failed', { error: error.message });
196
- if (this.client) {
197
- try { this.client.destroy(); } catch {}
198
- this.client = null;
199
- }
200
- throw error;
201
- }
202
- }
203
-
204
- async disconnect() {
205
- if (this.client) {
206
- try { this.client.destroy(); } catch {}
207
- this.client = null;
208
- }
209
- this.status = DISCORD_STATUS.DISCONNECTED;
210
- this.recentInteractions.clear();
211
- this.stickyAgent.clear();
212
- this.logger?.info('[DiscordService] Disconnected');
213
- }
214
-
215
- getStatus() {
216
- return {
217
- status: this.status,
218
- connected: this.status === DISCORD_STATUS.CONNECTED,
219
- botUsername: this.config.botUsername || null,
220
- botId: this.config.botId || null,
221
- guildCount: this.client?.guilds?.cache?.size || 0,
222
- channelMappingCount: Object.keys(this.channelMappings).length
223
- };
224
- }
225
-
226
- // --- Discord Event Handlers ---
227
-
228
- _setupHandlers() {
229
- if (!this.client) return;
230
-
231
- this.client.on('messageCreate', async (message) => {
232
- try {
233
- await this._handleMessage(message);
234
- } catch (error) {
235
- this.logger?.error('[DiscordService] Message handling error', { error: error.message });
236
- }
237
- });
238
-
239
- this.client.on('guildCreate', (guild) => {
240
- this.logger?.info('[DiscordService] Added to guild', { guildName: guild.name, guildId: guild.id });
241
- this._cacheGuildMetadata();
242
- });
243
-
244
- this.client.on('guildDelete', (guild) => {
245
- this.logger?.info('[DiscordService] Removed from guild', { guildId: guild.id });
246
- delete this.knownGuilds[guild.id];
247
- // Clean up channel mappings for this guild
248
- for (const key of Object.keys(this.channelMappings)) {
249
- if (key.startsWith(`${guild.id}:`)) {
250
- delete this.channelMappings[key];
251
- delete this.knownChannels[key];
252
- }
253
- }
254
- this._saveConfig().catch(() => {});
255
- });
256
-
257
- this.client.on('error', (error) => {
258
- this.logger?.error('[DiscordService] Client error', { error: error.message });
259
- });
260
- }
261
-
262
- _cacheGuildMetadata() {
263
- if (!this.client) return;
264
- const CT = this._ChannelType;
265
-
266
- for (const [guildId, guild] of this.client.guilds.cache) {
267
- this.knownGuilds[guildId] = {
268
- name: guild.name,
269
- icon: guild.iconURL({ size: 64 }) || null
270
- };
271
-
272
- for (const [channelId, channel] of guild.channels.cache) {
273
- // Text channels
274
- if (channel.type === CT?.GuildText || channel.type === 0) {
275
- const key = `${guildId}:${channelId}`;
276
- this.knownChannels[key] = {
277
- name: channel.name,
278
- guildId,
279
- guildName: guild.name,
280
- isThread: false
281
- };
282
- }
283
- // Public threads (type 11) and private threads (type 12)
284
- if (channel.type === CT?.PublicThread || channel.type === 11 ||
285
- channel.type === CT?.PrivateThread || channel.type === 12) {
286
- const key = `${guildId}:${channelId}`;
287
- this.knownChannels[key] = {
288
- name: channel.name,
289
- guildId,
290
- guildName: guild.name,
291
- isThread: true,
292
- parentId: channel.parentId || null,
293
- parentKey: channel.parentId ? `${guildId}:${channel.parentId}` : null
294
- };
295
- }
296
- }
297
- }
298
-
299
- this._saveConfig().catch(() => {});
300
- }
301
-
302
- // --- Message Routing ---
303
-
304
- /**
305
- * Build the routing key for a message.
306
- * For threads: check thread-specific mapping first, fall back to parent channel.
307
- * For channels: use the channel key directly.
308
- * Returns { routingKey, mappedAgents } or null if no mapping found.
309
- */
310
- _resolveRouting(message) {
311
- const guildId = message.guild.id;
312
- const channelId = message.channel.id;
313
- const directKey = `${guildId}:${channelId}`;
314
-
315
- // Check direct key first (works for both channels and thread-specific mappings)
316
- const directAgents = this.channelMappings[directKey];
317
- if (directAgents && directAgents.length > 0) {
318
- return { routingKey: directKey, mappedAgents: directAgents, respondToId: channelId };
319
- }
320
-
321
- // If this is a thread, fall back to parent channel mapping
322
- const parentId = message.channel.parentId;
323
- if (parentId) {
324
- const parentKey = `${guildId}:${parentId}`;
325
- const parentAgents = this.channelMappings[parentKey];
326
- if (parentAgents && parentAgents.length > 0) {
327
- // Use the thread's own ID for responses (reply in-thread), but routing comes from parent
328
- return { routingKey: directKey, mappedAgents: parentAgents, respondToId: channelId };
329
- }
330
- }
331
-
332
- return null;
333
- }
334
-
335
- async _handleMessage(message) {
336
- // Ignore bot messages (including self)
337
- if (message.author.bot) return;
338
- // Ignore DMs (only handle guild channels)
339
- if (!message.guild) return;
340
-
341
- // Resolve routing: thread-specific → parent channel fallback
342
- const routing = this._resolveRouting(message);
343
- if (!routing) return; // No agents mapped — ignore silently
344
-
345
- const { routingKey, mappedAgents } = routing;
346
-
347
- const messageText = message.content.trim();
348
- if (!messageText) return;
349
-
350
- let targetAgentId = null;
351
-
352
- if (mappedAgents.length === 1) {
353
- // Single agent — route directly
354
- targetAgentId = mappedAgents[0];
355
- } else {
356
- // Multiple agents — parse @agent-name mention
357
- targetAgentId = await this._resolveAgentFromMention(messageText, mappedAgents);
358
-
359
- if (!targetAgentId) {
360
- // Try sticky agent for this channel/thread
361
- const sticky = this.stickyAgent.get(routingKey);
362
- if (sticky && mappedAgents.includes(sticky)) {
363
- targetAgentId = sticky;
364
- }
365
- }
366
-
367
- if (!targetAgentId) {
368
- // Prompt user — list available agents
369
- const agentNames = [];
370
- for (const aid of mappedAgents) {
371
- const agent = this.agentPool ? await this.agentPool.getAgent(aid) : null;
372
- agentNames.push(agent?.name || aid);
373
- }
374
- await message.reply(
375
- `Multiple agents on this channel. Mention one by name:\n${agentNames.map(n => `\`@${n}\``).join(', ')}`
376
- );
377
- return;
378
- }
379
- }
380
-
381
- // Remove @agent-name prefix from the message if present
382
- const cleanedMessage = this._stripAgentMention(messageText, targetAgentId);
383
-
384
- // Update sticky agent for this routing key (channel or thread)
385
- this.stickyAgent.set(routingKey, targetAgentId);
386
-
387
- // Route to agent
388
- await this._routeToAgent(routingKey, targetAgentId, cleanedMessage, message);
389
- }
390
-
391
- async _resolveAgentFromMention(text, candidateAgentIds) {
392
- if (!this.agentPool) return null;
393
-
394
- // Check for @agent-name pattern at start of message
395
- const mentionMatch = text.match(/^@(\S+)/);
396
- if (!mentionMatch) return null;
397
-
398
- const mentionName = mentionMatch[1].toLowerCase();
399
-
400
- for (const agentId of candidateAgentIds) {
401
- const agent = await this.agentPool.getAgent(agentId);
402
- if (agent?.name?.toLowerCase() === mentionName) {
403
- return agentId;
404
- }
405
- }
406
-
407
- return null;
408
- }
409
-
410
- _stripAgentMention(text, agentId) {
411
- // Remove @agent-name from the start of the message
412
- return text.replace(/^@\S+\s*/, '').trim() || text;
413
- }
414
-
415
- async _routeToAgent(channelKey, agentId, text, discordMessage) {
416
- const sessionId = `discord-${channelKey.replace(':', '-')}`;
417
-
418
- // Record recent interaction for response targeting. `channelName` and
419
- // `guildName` are captured so we can surface a human-readable alias
420
- // to the agent (see `getBridgedChannels`).
421
- this.recentInteractions.set(`${agentId}:${channelKey}`, {
422
- channelKey,
423
- channelId: discordMessage.channel.id,
424
- channelName: discordMessage.channel.name || null,
425
- guildId: discordMessage.guild?.id ?? null,
426
- guildName: discordMessage.guild?.name ?? null,
427
- timestamp: Date.now()
428
- });
429
-
430
- // Show typing indicator
431
- try {
432
- await discordMessage.channel.sendTyping();
433
- } catch {}
434
-
435
- const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
436
- this.logger?.info('[DiscordService] Routing message to agent', {
437
- agentId,
438
- agentName: agent?.name,
439
- channelKey,
440
- messageLength: text.length
441
- });
442
-
443
- // Capture the source at ingress. `source` travels with the message
444
- // through the orchestrator -> messageProcessor -> agentPool pipeline and
445
- // becomes the human-readable `(Message by alice from Discord > …)` line
446
- // that the agent sees — see services/messageSource.js for the contract.
447
- const source = createDiscordSource(discordMessage);
448
-
449
- try {
450
- if (this.orchestrator) {
451
- await this.orchestrator.processRequest({
452
- interface: INTERFACE_TYPES.DISCORD,
453
- sessionId,
454
- action: 'send_message',
455
- payload: {
456
- agentId,
457
- message: text,
458
- streamingEnabled: false,
459
- source,
460
- }
461
- });
462
- } else if (this.agentPool) {
463
- // Fallback: direct message injection
464
- await this.agentPool.addUserMessage(agentId, {
465
- content: text,
466
- sessionId,
467
- source,
468
- });
469
- }
470
- } catch (error) {
471
- this.logger?.error('[DiscordService] Failed to route message', { error: error.message, agentId });
472
- try {
473
- await discordMessage.reply(`Failed to send message to agent: ${error.message}`);
474
- } catch {}
475
- }
476
- }
477
-
478
- // --- Broadcast Interception ---
479
-
480
- _interceptBroadcasts(wsManager) {
481
- if (!wsManager || this._originalBroadcast) return;
482
-
483
- // Chain onto existing wrapper (Telegram may already be wrapping)
484
- const currentBroadcast = wsManager.broadcastToSession.bind(wsManager);
485
- this._originalBroadcast = currentBroadcast;
486
-
487
- wsManager.broadcastToSession = (sessionId, message) => {
488
- // Call previous wrapper (Telegram's or original)
489
- currentBroadcast(sessionId, message);
490
- // Discord handling
491
- this._handleBroadcastEvent(sessionId, message);
492
- };
493
- }
494
-
495
- async _handleBroadcastEvent(sessionId, message) {
496
- if (!this.client || this.status !== DISCORD_STATUS.CONNECTED) return;
497
-
498
- const type = message?.type;
499
- if (!type) return;
500
-
501
- // Relay completed agent responses
502
- if (type === 'stream_complete') {
503
- await this._relayAgentResponse(message);
504
- return;
505
- }
506
-
507
- // Relay user prompt requests
508
- if (type === 'user_prompt_request') {
509
- await this._relayPromptRequest(message);
510
- return;
511
- }
512
- }
513
-
514
- async _relayAgentResponse(message) {
515
- const agentId = message.agentId || message.data?.agentId;
516
- if (!agentId) return;
517
-
518
- const content = message.content || message.data?.content ||
519
- message.message?.content || message.data?.message?.content;
520
- if (!content) return;
521
-
522
- // Skip tool-result messages and user messages
523
- const role = message.role || message.data?.role || message.message?.role;
524
- if (role === 'user' || role === 'tool') return;
525
-
526
- // Parse every <external>…</external> block out of the raw content.
527
- // Content inside the tags is relayed verbatim — no stripping, no
528
- // content-type discrimination. The web UI still sees the full raw
529
- // message via the WS broadcast and is unaffected by this filter.
530
- const { blocks } = filterContentForExternalRelay(content);
531
- if (blocks.length === 0) {
532
- this.logger?.debug?.('[DiscordService] no <external> blocks in agent response — nothing relayed', { agentId });
533
- return;
534
- }
535
-
536
- // Collect every live Discord channel bridged to this agent. Drops
537
- // stale entries past the interaction TTL as a side-effect.
538
- const bridged = this._getBridgedChannelEntries(agentId);
539
- if (bridged.length === 0) return;
540
-
541
- // Map owned aliases → entry so we can look up channelId quickly after
542
- // `resolveBlockTargets` tells us which aliases to send to.
543
- const aliasToEntry = new Map(bridged.map(e => [e.alias, e]));
544
- const ownedAliases = [...aliasToEntry.keys()];
545
-
546
- const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
547
- const agentName = agent?.name || agentId;
548
-
549
- for (const block of blocks) {
550
- const targets = resolveBlockTargets(block, ownedAliases);
551
- for (const alias of targets) {
552
- const entry = aliasToEntry.get(alias);
553
- if (!entry) continue;
554
- try {
555
- await this._sendFormattedResponse(entry.channelId, agentName, block.text);
556
- } catch (error) {
557
- this.logger?.warn('[DiscordService] Failed to relay block', {
558
- alias, channelId: entry.channelId, error: error.message
559
- });
560
- }
561
- }
562
- }
563
- }
564
-
565
- /**
566
- * Active Discord bridges for an agent, as entries tied to the relay
567
- * pipeline. Each entry carries an `alias` the agent can use in
568
- * `<external to="…">`. Expired entries (past INTERACTION_TTL_MS) are
569
- * garbage-collected during the scan so alias lists stay honest.
570
- *
571
- * @param {string} agentId
572
- * @returns {Array<{alias: string, label: string, channelKey: string, channelId: string, guildId: string|null, guildName: string|null, channelName: string|null}>}
573
- * @private
574
- */
575
- _getBridgedChannelEntries(agentId) {
576
- const now = Date.now();
577
- const entries = [];
578
- for (const [key, interaction] of this.recentInteractions) {
579
- if (!key.startsWith(`${agentId}:`)) continue;
580
- if (now - interaction.timestamp > INTERACTION_TTL_MS) {
581
- this.recentInteractions.delete(key);
582
- continue;
583
- }
584
- const channel = interaction.channelName || interaction.channelId;
585
- // Alias format: `discord:#<channel>` when we have a channel name;
586
- // otherwise fall back to the bare channel id. Substring matching in
587
- // `resolveBlockTargets` means the agent can write short forms like
588
- // `discord:#ops` even if the canonical alias is longer.
589
- const alias = interaction.channelName
590
- ? `discord:#${interaction.channelName}`
591
- : `discord:${interaction.channelId}`;
592
- const label = interaction.guildName
593
- ? `Discord channel #${interaction.channelName ?? interaction.channelId} in ${interaction.guildName}`
594
- : `Discord channel ${interaction.channelName ?? interaction.channelId}`;
595
- entries.push({
596
- alias, label,
597
- channelKey: interaction.channelKey,
598
- channelId: interaction.channelId,
599
- guildId: interaction.guildId,
600
- guildName: interaction.guildName,
601
- channelName: interaction.channelName,
602
- });
603
- }
604
- return entries;
605
- }
606
-
607
- /**
608
- * Compact alias list the scheduler surfaces in the system prompt so the
609
- * agent can address specific Discord channels via `<external to="…">`.
610
- *
611
- * @param {string} agentId
612
- * @returns {Array<{alias: string, label: string}>}
613
- */
614
- getBridgedChannels(agentId) {
615
- if (!agentId || this.status !== DISCORD_STATUS.CONNECTED) return [];
616
- return this._getBridgedChannelEntries(agentId).map(e => ({ alias: e.alias, label: e.label }));
617
- }
618
-
619
- /**
620
- * True when the agent has at least one live Discord bridge. Scheduler
621
- * uses this to decide whether to inject the `<external>` prompt
622
- * guidance for this turn (cheaper than calling getBridgedChannels when
623
- * we only need a boolean).
624
- */
625
- isAgentBridged(agentId) {
626
- if (!agentId || this.status !== DISCORD_STATUS.CONNECTED) return false;
627
- const now = Date.now();
628
- for (const [key, interaction] of this.recentInteractions) {
629
- if (!key.startsWith(`${agentId}:`)) continue;
630
- if (now - interaction.timestamp <= INTERACTION_TTL_MS) return true;
631
- }
632
- return false;
633
- }
634
-
635
- async _relayPromptRequest(message) {
636
- const agentId = message.data?.agentId || message.agentId;
637
- if (!agentId) return;
638
-
639
- const promptText = message.data?.prompt || message.data?.message || 'Agent is requesting input';
640
-
641
- for (const [key, interaction] of this.recentInteractions) {
642
- if (!key.startsWith(`${agentId}:`)) continue;
643
- if (Date.now() - interaction.timestamp > INTERACTION_TTL_MS) continue;
644
-
645
- try {
646
- const channel = await this.client.channels.fetch(interaction.channelId);
647
- if (channel) {
648
- await channel.send(`**Input needed:**\n${promptText}`);
649
- }
650
- } catch {}
651
- }
652
- }
653
-
654
- // --- Response Formatting ---
655
-
656
- async _sendFormattedResponse(channelId, agentName, content) {
657
- const channel = await this.client.channels.fetch(channelId);
658
- if (!channel) return;
659
-
660
- const header = `**${agentName}:**\n`;
661
- const fullMessage = header + content;
662
-
663
- // Split if needed
664
- const parts = this._splitMessage(fullMessage);
665
-
666
- for (const part of parts) {
667
- await channel.send(part);
668
- }
669
- }
670
-
671
- _splitMessage(text) {
672
- if (text.length <= MAX_MESSAGE_LENGTH) return [text];
673
-
674
- const parts = [];
675
- let remaining = text;
676
-
677
- while (remaining.length > 0) {
678
- if (remaining.length <= MAX_MESSAGE_LENGTH) {
679
- parts.push(remaining);
680
- break;
681
- }
682
-
683
- // Try to split at code block boundary
684
- let splitIndex = remaining.lastIndexOf('\n```', MAX_MESSAGE_LENGTH);
685
- if (splitIndex > MAX_MESSAGE_LENGTH * 0.3) {
686
- splitIndex += 1; // Include the newline
687
- } else {
688
- // Try newline
689
- splitIndex = remaining.lastIndexOf('\n', MAX_MESSAGE_LENGTH);
690
- if (splitIndex < MAX_MESSAGE_LENGTH * 0.3) {
691
- // Try space
692
- splitIndex = remaining.lastIndexOf(' ', MAX_MESSAGE_LENGTH);
693
- if (splitIndex < MAX_MESSAGE_LENGTH * 0.3) {
694
- // Hard split
695
- splitIndex = MAX_MESSAGE_LENGTH;
696
- }
697
- }
698
- }
699
-
700
- parts.push(remaining.slice(0, splitIndex));
701
- remaining = remaining.slice(splitIndex).trimStart();
702
- }
703
-
704
- return parts;
705
- }
706
-
707
- // --- Channel Mapping Management ---
708
-
709
- async assignAgentToChannel(channelKey, agentId) {
710
- if (!this.channelMappings[channelKey]) {
711
- this.channelMappings[channelKey] = [];
712
- }
713
- if (!this.channelMappings[channelKey].includes(agentId)) {
714
- this.channelMappings[channelKey].push(agentId);
715
- }
716
- await this._saveConfig();
717
- this.logger?.info('[DiscordService] Agent assigned to channel', { channelKey, agentId });
718
- }
719
-
720
- async removeAgentFromChannel(channelKey, agentId) {
721
- if (this.channelMappings[channelKey]) {
722
- this.channelMappings[channelKey] = this.channelMappings[channelKey].filter(id => id !== agentId);
723
- if (this.channelMappings[channelKey].length === 0) {
724
- delete this.channelMappings[channelKey];
725
- }
726
- }
727
- // Clean sticky if removed agent was sticky
728
- if (this.stickyAgent.get(channelKey) === agentId) {
729
- this.stickyAgent.delete(channelKey);
730
- }
731
- await this._saveConfig();
732
- this.logger?.info('[DiscordService] Agent removed from channel', { channelKey, agentId });
733
- }
734
-
735
- getChannelMappings() {
736
- return {
737
- mappings: this.channelMappings,
738
- knownGuilds: this.knownGuilds,
739
- knownChannels: this.knownChannels
740
- };
741
- }
742
-
743
- getAvailableChannels() {
744
- if (!this.client || this.status !== DISCORD_STATUS.CONNECTED) {
745
- // Return cached channels if disconnected
746
- return Object.entries(this.knownChannels).map(([key, ch]) => ({
747
- key,
748
- channelId: key.split(':')[1],
749
- guildId: key.split(':')[0],
750
- name: ch.name,
751
- guildName: ch.guildName,
752
- isThread: ch.isThread || false,
753
- parentKey: ch.parentKey || null
754
- }));
755
- }
756
-
757
- const channels = [];
758
- const CT = this._ChannelType;
759
- for (const [guildId, guild] of this.client.guilds.cache) {
760
- for (const [channelId, channel] of guild.channels.cache) {
761
- const isText = channel.type === CT?.GuildText || channel.type === 0;
762
- const isThread = channel.type === CT?.PublicThread || channel.type === 11 ||
763
- channel.type === CT?.PrivateThread || channel.type === 12;
764
- if (isText || isThread) {
765
- const key = `${guildId}:${channelId}`;
766
- channels.push({
767
- key,
768
- channelId,
769
- guildId,
770
- name: channel.name,
771
- guildName: guild.name,
772
- isThread,
773
- parentKey: isThread && channel.parentId ? `${guildId}:${channel.parentId}` : null
774
- });
775
- }
776
- }
777
- }
778
- return channels;
779
- }
780
- }
781
-
782
- // Singleton pattern (matches Telegram)
783
- let instance = null;
784
-
785
- export function getDiscordService(logger) {
786
- if (!instance) {
787
- instance = new DiscordService(logger);
788
- }
789
- return instance;
790
- }
791
-
792
- export { DiscordService, DISCORD_STATUS };
793
- export default DiscordService;
1
+ /**
2
+ * DiscordService — Remote agent interface via Discord Bot
3
+ *
4
+ * Purpose:
5
+ * - Conversational interface with agents from Discord channels
6
+ * - Many-to-many mapping: each agent can be on 0..N channels, each channel can have 0..N agents
7
+ * - @agent-name mention routing (single-agent channels route directly)
8
+ * - Smart response formatting (markdown, code blocks, message splitting)
9
+ * - Broadcast interception for real-time agent response relay
10
+ *
11
+ * Architecture:
12
+ * - Uses discord.js v14+ (lazy-imported, optional dependency)
13
+ * - Gateway intents: Guilds, GuildMessages, MessageContent (privileged)
14
+ * - Intercepts WebSocket broadcasts to capture agent responses
15
+ * - Routes user messages to agents via orchestrator.processRequest()
16
+ * - Persists config to {userData}/discord/discord-config.json
17
+ */
18
+
19
+ import { promises as fs } from 'fs';
20
+ import path from 'path';
21
+ import { getUserDataPaths, ensureUserDataDirs } from '../utilities/userDataDir.js';
22
+ import { INTERFACE_TYPES } from '../utilities/constants.js';
23
+ import { filterContentForExternalRelay, resolveBlockTargets } from './channelFilter.js';
24
+ import { createDiscordSource } from './messageSource.js';
25
+
26
+ const DISCORD_STATUS = {
27
+ DISCONNECTED: 'disconnected',
28
+ CONNECTING: 'connecting',
29
+ CONNECTED: 'connected',
30
+ FAILED: 'failed'
31
+ };
32
+
33
+ const MAX_MESSAGE_LENGTH = 1950; // Discord limit is 2000, leave room for formatting
34
+ const INTERACTION_TTL_MS = 30 * 60 * 1000; // 30 minutes — only relay to channels with recent interaction
35
+
36
+ class DiscordService {
37
+ constructor(logger = null) {
38
+ this.logger = logger;
39
+
40
+ // Dependencies (set via setters)
41
+ this.orchestrator = null;
42
+ this.agentPool = null;
43
+ this.webSocketManager = null;
44
+ this.flowExecutor = null;
45
+
46
+ // Bot state
47
+ this.client = null;
48
+ this.status = DISCORD_STATUS.DISCONNECTED;
49
+
50
+ // Channel-agent mappings: channelKey -> [agentId]
51
+ // channelKey format: "guildId:channelId" or "guildId:threadId"
52
+ // Both channels and threads use the same mapping structure.
53
+ // When a message arrives in a thread, we check the thread key first,
54
+ // then fall back to the parent channel key.
55
+ this.channelMappings = {};
56
+
57
+ // Track recent interactions: "agentId:routingKey" -> { channelKey, timestamp }
58
+ // Used to determine where to relay agent responses
59
+ this.recentInteractions = new Map();
60
+
61
+ // Sticky agent per routing key: routingKey -> lastAgentId
62
+ this.stickyAgent = new Map();
63
+
64
+ // Cached Discord metadata for UI display
65
+ this.knownGuilds = {};
66
+ this.knownChannels = {};
67
+
68
+ // Config persistence
69
+ this.dataDir = null;
70
+ this.configPath = null;
71
+ this.config = {};
72
+
73
+ // Original broadcast (saved before wrapping)
74
+ this._originalBroadcast = null;
75
+ }
76
+
77
+ // --- Dependency Injection ---
78
+
79
+ setOrchestrator(orchestrator) { this.orchestrator = orchestrator; }
80
+ setAgentPool(agentPool) { this.agentPool = agentPool; }
81
+ setWebSocketManager(wsManager) {
82
+ this.webSocketManager = wsManager;
83
+ this._interceptBroadcasts(wsManager);
84
+ }
85
+ setFlowExecutor(flowExecutor) { this.flowExecutor = flowExecutor; }
86
+
87
+ // --- Config Persistence ---
88
+
89
+ async _ensureDataDir() {
90
+ if (!this.dataDir) {
91
+ await ensureUserDataDirs();
92
+ const paths = getUserDataPaths();
93
+ this.dataDir = path.join(paths.base, 'discord');
94
+ this.configPath = path.join(this.dataDir, 'discord-config.json');
95
+ await fs.mkdir(this.dataDir, { recursive: true });
96
+ }
97
+ }
98
+
99
+ async _loadConfig() {
100
+ await this._ensureDataDir();
101
+ try {
102
+ const data = await fs.readFile(this.configPath, 'utf8');
103
+ this.config = JSON.parse(data);
104
+ this.channelMappings = this.config.channelMappings || {};
105
+ this.knownGuilds = this.config.knownGuilds || {};
106
+ this.knownChannels = this.config.knownChannels || {};
107
+ } catch {
108
+ this.config = {};
109
+ }
110
+ }
111
+
112
+ async _saveConfig() {
113
+ await this._ensureDataDir();
114
+ this.config.channelMappings = this.channelMappings;
115
+ this.config.knownGuilds = this.knownGuilds;
116
+ this.config.knownChannels = this.knownChannels;
117
+ this.config.updatedAt = new Date().toISOString();
118
+ await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
119
+ }
120
+
121
+ // --- Lifecycle ---
122
+
123
+ async autoConnect() {
124
+ await this._loadConfig();
125
+ if (this.config.botToken) {
126
+ try {
127
+ await this.connect(this.config.botToken);
128
+ } catch (error) {
129
+ this.logger?.warn('[DiscordService] Auto-connect failed', { error: error.message });
130
+ }
131
+ }
132
+ }
133
+
134
+ async connect(botToken) {
135
+ if (this.status === DISCORD_STATUS.CONNECTED) {
136
+ await this.disconnect();
137
+ }
138
+
139
+ this.status = DISCORD_STATUS.CONNECTING;
140
+ this.logger?.info('[DiscordService] Connecting...');
141
+
142
+ try {
143
+ // Lazy import discord.js (optional dependency)
144
+ const { Client, GatewayIntentBits, ChannelType } = await import('discord.js');
145
+ this._ChannelType = ChannelType;
146
+
147
+ this.client = new Client({
148
+ intents: [
149
+ GatewayIntentBits.Guilds,
150
+ GatewayIntentBits.GuildMessages,
151
+ GatewayIntentBits.MessageContent
152
+ ]
153
+ });
154
+
155
+ // Wait for ready event
156
+ await new Promise((resolve, reject) => {
157
+ const timeout = setTimeout(() => reject(new Error('Discord login timed out after 30s')), 30000);
158
+
159
+ this.client.once('ready', () => {
160
+ clearTimeout(timeout);
161
+ resolve();
162
+ });
163
+
164
+ this.client.once('error', (err) => {
165
+ clearTimeout(timeout);
166
+ reject(err);
167
+ });
168
+
169
+ this.client.login(botToken).catch(reject);
170
+ });
171
+
172
+ const botUser = this.client.user;
173
+ this.logger?.info('[DiscordService] Connected', {
174
+ botName: botUser.username,
175
+ guildCount: this.client.guilds.cache.size
176
+ });
177
+
178
+ // Save config
179
+ this.config.botToken = botToken;
180
+ this.config.botUsername = botUser.username;
181
+ this.config.botId = botUser.id;
182
+ await this._saveConfig();
183
+
184
+ // Cache guild/channel metadata
185
+ this._cacheGuildMetadata();
186
+
187
+ // Setup event handlers
188
+ this._setupHandlers();
189
+
190
+ this.status = DISCORD_STATUS.CONNECTED;
191
+
192
+ return { username: botUser.username, id: botUser.id };
193
+ } catch (error) {
194
+ this.status = DISCORD_STATUS.FAILED;
195
+ this.logger?.error('[DiscordService] Connection failed', { error: error.message });
196
+ if (this.client) {
197
+ try { this.client.destroy(); } catch { /* swallow */ }
198
+ this.client = null;
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ async disconnect() {
205
+ if (this.client) {
206
+ try { this.client.destroy(); } catch { /* swallow */ }
207
+ this.client = null;
208
+ }
209
+ this.status = DISCORD_STATUS.DISCONNECTED;
210
+ this.recentInteractions.clear();
211
+ this.stickyAgent.clear();
212
+ this.logger?.info('[DiscordService] Disconnected');
213
+ }
214
+
215
+ getStatus() {
216
+ return {
217
+ status: this.status,
218
+ connected: this.status === DISCORD_STATUS.CONNECTED,
219
+ botUsername: this.config.botUsername || null,
220
+ botId: this.config.botId || null,
221
+ guildCount: this.client?.guilds?.cache?.size || 0,
222
+ channelMappingCount: Object.keys(this.channelMappings).length
223
+ };
224
+ }
225
+
226
+ // --- Discord Event Handlers ---
227
+
228
+ _setupHandlers() {
229
+ if (!this.client) return;
230
+
231
+ this.client.on('messageCreate', async (message) => {
232
+ try {
233
+ await this._handleMessage(message);
234
+ } catch (error) {
235
+ this.logger?.error('[DiscordService] Message handling error', { error: error.message });
236
+ }
237
+ });
238
+
239
+ this.client.on('guildCreate', (guild) => {
240
+ this.logger?.info('[DiscordService] Added to guild', { guildName: guild.name, guildId: guild.id });
241
+ this._cacheGuildMetadata();
242
+ });
243
+
244
+ this.client.on('guildDelete', (guild) => {
245
+ this.logger?.info('[DiscordService] Removed from guild', { guildId: guild.id });
246
+ delete this.knownGuilds[guild.id];
247
+ // Clean up channel mappings for this guild
248
+ for (const key of Object.keys(this.channelMappings)) {
249
+ if (key.startsWith(`${guild.id}:`)) {
250
+ delete this.channelMappings[key];
251
+ delete this.knownChannels[key];
252
+ }
253
+ }
254
+ this._saveConfig().catch(() => {});
255
+ });
256
+
257
+ this.client.on('error', (error) => {
258
+ this.logger?.error('[DiscordService] Client error', { error: error.message });
259
+ });
260
+ }
261
+
262
+ _cacheGuildMetadata() {
263
+ if (!this.client) return;
264
+ const CT = this._ChannelType;
265
+
266
+ for (const [guildId, guild] of this.client.guilds.cache) {
267
+ this.knownGuilds[guildId] = {
268
+ name: guild.name,
269
+ icon: guild.iconURL({ size: 64 }) || null
270
+ };
271
+
272
+ for (const [channelId, channel] of guild.channels.cache) {
273
+ // Text channels
274
+ if (channel.type === CT?.GuildText || channel.type === 0) {
275
+ const key = `${guildId}:${channelId}`;
276
+ this.knownChannels[key] = {
277
+ name: channel.name,
278
+ guildId,
279
+ guildName: guild.name,
280
+ isThread: false
281
+ };
282
+ }
283
+ // Public threads (type 11) and private threads (type 12)
284
+ if (channel.type === CT?.PublicThread || channel.type === 11 ||
285
+ channel.type === CT?.PrivateThread || channel.type === 12) {
286
+ const key = `${guildId}:${channelId}`;
287
+ this.knownChannels[key] = {
288
+ name: channel.name,
289
+ guildId,
290
+ guildName: guild.name,
291
+ isThread: true,
292
+ parentId: channel.parentId || null,
293
+ parentKey: channel.parentId ? `${guildId}:${channel.parentId}` : null
294
+ };
295
+ }
296
+ }
297
+ }
298
+
299
+ this._saveConfig().catch(() => {});
300
+ }
301
+
302
+ // --- Message Routing ---
303
+
304
+ /**
305
+ * Build the routing key for a message.
306
+ * For threads: check thread-specific mapping first, fall back to parent channel.
307
+ * For channels: use the channel key directly.
308
+ * Returns { routingKey, mappedAgents } or null if no mapping found.
309
+ */
310
+ _resolveRouting(message) {
311
+ const guildId = message.guild.id;
312
+ const channelId = message.channel.id;
313
+ const directKey = `${guildId}:${channelId}`;
314
+
315
+ // Check direct key first (works for both channels and thread-specific mappings)
316
+ const directAgents = this.channelMappings[directKey];
317
+ if (directAgents && directAgents.length > 0) {
318
+ return { routingKey: directKey, mappedAgents: directAgents, respondToId: channelId };
319
+ }
320
+
321
+ // If this is a thread, fall back to parent channel mapping
322
+ const parentId = message.channel.parentId;
323
+ if (parentId) {
324
+ const parentKey = `${guildId}:${parentId}`;
325
+ const parentAgents = this.channelMappings[parentKey];
326
+ if (parentAgents && parentAgents.length > 0) {
327
+ // Use the thread's own ID for responses (reply in-thread), but routing comes from parent
328
+ return { routingKey: directKey, mappedAgents: parentAgents, respondToId: channelId };
329
+ }
330
+ }
331
+
332
+ return null;
333
+ }
334
+
335
+ async _handleMessage(message) {
336
+ // Ignore bot messages (including self)
337
+ if (message.author.bot) return;
338
+ // Ignore DMs (only handle guild channels)
339
+ if (!message.guild) return;
340
+
341
+ // Resolve routing: thread-specific → parent channel fallback
342
+ const routing = this._resolveRouting(message);
343
+ if (!routing) return; // No agents mapped — ignore silently
344
+
345
+ const { routingKey, mappedAgents } = routing;
346
+
347
+ const messageText = message.content.trim();
348
+ if (!messageText) return;
349
+
350
+ let targetAgentId;
351
+
352
+ if (mappedAgents.length === 1) {
353
+ // Single agent — route directly
354
+ targetAgentId = mappedAgents[0];
355
+ } else {
356
+ // Multiple agents — parse @agent-name mention
357
+ targetAgentId = await this._resolveAgentFromMention(messageText, mappedAgents);
358
+
359
+ if (!targetAgentId) {
360
+ // Try sticky agent for this channel/thread
361
+ const sticky = this.stickyAgent.get(routingKey);
362
+ if (sticky && mappedAgents.includes(sticky)) {
363
+ targetAgentId = sticky;
364
+ }
365
+ }
366
+
367
+ if (!targetAgentId) {
368
+ // Prompt user — list available agents
369
+ const agentNames = [];
370
+ for (const aid of mappedAgents) {
371
+ const agent = this.agentPool ? await this.agentPool.getAgent(aid) : null;
372
+ agentNames.push(agent?.name || aid);
373
+ }
374
+ await message.reply(
375
+ `Multiple agents on this channel. Mention one by name:\n${agentNames.map(n => `\`@${n}\``).join(', ')}`
376
+ );
377
+ return;
378
+ }
379
+ }
380
+
381
+ // Remove @agent-name prefix from the message if present
382
+ const cleanedMessage = this._stripAgentMention(messageText);
383
+
384
+ // Update sticky agent for this routing key (channel or thread)
385
+ this.stickyAgent.set(routingKey, targetAgentId);
386
+
387
+ // Route to agent
388
+ await this._routeToAgent(routingKey, targetAgentId, cleanedMessage, message);
389
+ }
390
+
391
+ async _resolveAgentFromMention(text, candidateAgentIds) {
392
+ if (!this.agentPool) return null;
393
+
394
+ // Check for @agent-name pattern at start of message
395
+ const mentionMatch = text.match(/^@(\S+)/);
396
+ if (!mentionMatch) return null;
397
+
398
+ const mentionName = mentionMatch[1].toLowerCase();
399
+
400
+ for (const agentId of candidateAgentIds) {
401
+ const agent = await this.agentPool.getAgent(agentId);
402
+ if (agent?.name?.toLowerCase() === mentionName) {
403
+ return agentId;
404
+ }
405
+ }
406
+
407
+ return null;
408
+ }
409
+
410
+ _stripAgentMention(text) {
411
+ // Remove @agent-name from the start of the message
412
+ return text.replace(/^@\S+\s*/, '').trim() || text;
413
+ }
414
+
415
+ async _routeToAgent(channelKey, agentId, text, discordMessage) {
416
+ const sessionId = `discord-${channelKey.replace(':', '-')}`;
417
+
418
+ // Record recent interaction for response targeting. `channelName` and
419
+ // `guildName` are captured so we can surface a human-readable alias
420
+ // to the agent (see `getBridgedChannels`).
421
+ this.recentInteractions.set(`${agentId}:${channelKey}`, {
422
+ channelKey,
423
+ channelId: discordMessage.channel.id,
424
+ channelName: discordMessage.channel.name || null,
425
+ guildId: discordMessage.guild?.id ?? null,
426
+ guildName: discordMessage.guild?.name ?? null,
427
+ timestamp: Date.now()
428
+ });
429
+
430
+ // Show typing indicator
431
+ try {
432
+ await discordMessage.channel.sendTyping();
433
+ } catch { /* swallow */ }
434
+
435
+ const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
436
+ this.logger?.info('[DiscordService] Routing message to agent', {
437
+ agentId,
438
+ agentName: agent?.name,
439
+ channelKey,
440
+ messageLength: text.length
441
+ });
442
+
443
+ // Capture the source at ingress. `source` travels with the message
444
+ // through the orchestrator -> messageProcessor -> agentPool pipeline and
445
+ // becomes the human-readable `(Message by alice from Discord > …)` line
446
+ // that the agent sees — see services/messageSource.js for the contract.
447
+ const source = createDiscordSource(discordMessage);
448
+
449
+ try {
450
+ if (this.orchestrator) {
451
+ await this.orchestrator.processRequest({
452
+ interface: INTERFACE_TYPES.DISCORD,
453
+ sessionId,
454
+ action: 'send_message',
455
+ payload: {
456
+ agentId,
457
+ message: text,
458
+ streamingEnabled: false,
459
+ source,
460
+ }
461
+ });
462
+ } else if (this.agentPool) {
463
+ // Fallback: direct message injection
464
+ await this.agentPool.addUserMessage(agentId, {
465
+ content: text,
466
+ sessionId,
467
+ source,
468
+ });
469
+ }
470
+ } catch (error) {
471
+ this.logger?.error('[DiscordService] Failed to route message', { error: error.message, agentId });
472
+ try {
473
+ await discordMessage.reply(`Failed to send message to agent: ${error.message}`);
474
+ } catch { /* swallow */ }
475
+ }
476
+ }
477
+
478
+ // --- Broadcast Interception ---
479
+
480
+ _interceptBroadcasts(wsManager) {
481
+ if (!wsManager || this._originalBroadcast) return;
482
+
483
+ // Chain onto existing wrapper (Telegram may already be wrapping)
484
+ const currentBroadcast = wsManager.broadcastToSession.bind(wsManager);
485
+ this._originalBroadcast = currentBroadcast;
486
+
487
+ wsManager.broadcastToSession = (sessionId, message) => {
488
+ // Call previous wrapper (Telegram's or original)
489
+ currentBroadcast(sessionId, message);
490
+ // Discord handling
491
+ this._handleBroadcastEvent(sessionId, message);
492
+ };
493
+ }
494
+
495
+ async _handleBroadcastEvent(sessionId, message) {
496
+ if (!this.client || this.status !== DISCORD_STATUS.CONNECTED) return;
497
+
498
+ const type = message?.type;
499
+ if (!type) return;
500
+
501
+ // Relay completed agent responses
502
+ if (type === 'stream_complete') {
503
+ await this._relayAgentResponse(message);
504
+ return;
505
+ }
506
+
507
+ // Relay user prompt requests
508
+ if (type === 'user_prompt_request') {
509
+ await this._relayPromptRequest(message);
510
+ return;
511
+ }
512
+ }
513
+
514
+ async _relayAgentResponse(message) {
515
+ const agentId = message.agentId || message.data?.agentId;
516
+ if (!agentId) return;
517
+
518
+ const content = message.content || message.data?.content ||
519
+ message.message?.content || message.data?.message?.content;
520
+ if (!content) return;
521
+
522
+ // Skip tool-result messages and user messages
523
+ const role = message.role || message.data?.role || message.message?.role;
524
+ if (role === 'user' || role === 'tool') return;
525
+
526
+ // Parse every <external>…</external> block out of the raw content.
527
+ // Content inside the tags is relayed verbatim — no stripping, no
528
+ // content-type discrimination. The web UI still sees the full raw
529
+ // message via the WS broadcast and is unaffected by this filter.
530
+ const { blocks } = filterContentForExternalRelay(content);
531
+ if (blocks.length === 0) {
532
+ this.logger?.debug?.('[DiscordService] no <external> blocks in agent response — nothing relayed', { agentId });
533
+ return;
534
+ }
535
+
536
+ // Collect every live Discord channel bridged to this agent. Drops
537
+ // stale entries past the interaction TTL as a side-effect.
538
+ const bridged = this._getBridgedChannelEntries(agentId);
539
+ if (bridged.length === 0) return;
540
+
541
+ // Map owned aliases → entry so we can look up channelId quickly after
542
+ // `resolveBlockTargets` tells us which aliases to send to.
543
+ const aliasToEntry = new Map(bridged.map(e => [e.alias, e]));
544
+ const ownedAliases = [...aliasToEntry.keys()];
545
+
546
+ const agent = this.agentPool ? await this.agentPool.getAgent(agentId) : null;
547
+ const agentName = agent?.name || agentId;
548
+
549
+ for (const block of blocks) {
550
+ const targets = resolveBlockTargets(block, ownedAliases);
551
+ for (const alias of targets) {
552
+ const entry = aliasToEntry.get(alias);
553
+ if (!entry) continue;
554
+ try {
555
+ await this._sendFormattedResponse(entry.channelId, agentName, block.text);
556
+ } catch (error) {
557
+ this.logger?.warn('[DiscordService] Failed to relay block', {
558
+ alias, channelId: entry.channelId, error: error.message
559
+ });
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Active Discord bridges for an agent, as entries tied to the relay
567
+ * pipeline. Each entry carries an `alias` the agent can use in
568
+ * `<external to="…">`. Expired entries (past INTERACTION_TTL_MS) are
569
+ * garbage-collected during the scan so alias lists stay honest.
570
+ *
571
+ * @param {string} agentId
572
+ * @returns {Array<{alias: string, label: string, channelKey: string, channelId: string, guildId: string|null, guildName: string|null, channelName: string|null}>}
573
+ * @private
574
+ */
575
+ _getBridgedChannelEntries(agentId) {
576
+ const now = Date.now();
577
+ const entries = [];
578
+ for (const [key, interaction] of this.recentInteractions) {
579
+ if (!key.startsWith(`${agentId}:`)) continue;
580
+ if (now - interaction.timestamp > INTERACTION_TTL_MS) {
581
+ this.recentInteractions.delete(key);
582
+ continue;
583
+ }
584
+ // Alias format: `discord:#<channel>` when we have a channel name;
585
+ // otherwise fall back to the bare channel id. Substring matching in
586
+ // `resolveBlockTargets` means the agent can write short forms like
587
+ // `discord:#ops` even if the canonical alias is longer.
588
+ const alias = interaction.channelName
589
+ ? `discord:#${interaction.channelName}`
590
+ : `discord:${interaction.channelId}`;
591
+ const label = interaction.guildName
592
+ ? `Discord channel #${interaction.channelName ?? interaction.channelId} in ${interaction.guildName}`
593
+ : `Discord channel ${interaction.channelName ?? interaction.channelId}`;
594
+ entries.push({
595
+ alias, label,
596
+ channelKey: interaction.channelKey,
597
+ channelId: interaction.channelId,
598
+ guildId: interaction.guildId,
599
+ guildName: interaction.guildName,
600
+ channelName: interaction.channelName,
601
+ });
602
+ }
603
+ return entries;
604
+ }
605
+
606
+ /**
607
+ * Compact alias list the scheduler surfaces in the system prompt so the
608
+ * agent can address specific Discord channels via `<external to="…">`.
609
+ *
610
+ * @param {string} agentId
611
+ * @returns {Array<{alias: string, label: string}>}
612
+ */
613
+ getBridgedChannels(agentId) {
614
+ if (!agentId || this.status !== DISCORD_STATUS.CONNECTED) return [];
615
+ return this._getBridgedChannelEntries(agentId).map(e => ({ alias: e.alias, label: e.label }));
616
+ }
617
+
618
+ /**
619
+ * True when the agent has at least one live Discord bridge. Scheduler
620
+ * uses this to decide whether to inject the `<external>` prompt
621
+ * guidance for this turn (cheaper than calling getBridgedChannels when
622
+ * we only need a boolean).
623
+ */
624
+ isAgentBridged(agentId) {
625
+ if (!agentId || this.status !== DISCORD_STATUS.CONNECTED) return false;
626
+ const now = Date.now();
627
+ for (const [key, interaction] of this.recentInteractions) {
628
+ if (!key.startsWith(`${agentId}:`)) continue;
629
+ if (now - interaction.timestamp <= INTERACTION_TTL_MS) return true;
630
+ }
631
+ return false;
632
+ }
633
+
634
+ async _relayPromptRequest(message) {
635
+ const agentId = message.data?.agentId || message.agentId;
636
+ if (!agentId) return;
637
+
638
+ const promptText = message.data?.prompt || message.data?.message || 'Agent is requesting input';
639
+
640
+ for (const [key, interaction] of this.recentInteractions) {
641
+ if (!key.startsWith(`${agentId}:`)) continue;
642
+ if (Date.now() - interaction.timestamp > INTERACTION_TTL_MS) continue;
643
+
644
+ try {
645
+ const channel = await this.client.channels.fetch(interaction.channelId);
646
+ if (channel) {
647
+ await channel.send(`**Input needed:**\n${promptText}`);
648
+ }
649
+ } catch { /* swallow */ }
650
+ }
651
+ }
652
+
653
+ // --- Response Formatting ---
654
+
655
+ async _sendFormattedResponse(channelId, agentName, content) {
656
+ const channel = await this.client.channels.fetch(channelId);
657
+ if (!channel) return;
658
+
659
+ const header = `**${agentName}:**\n`;
660
+ const fullMessage = header + content;
661
+
662
+ // Split if needed
663
+ const parts = this._splitMessage(fullMessage);
664
+
665
+ for (const part of parts) {
666
+ await channel.send(part);
667
+ }
668
+ }
669
+
670
+ _splitMessage(text) {
671
+ if (text.length <= MAX_MESSAGE_LENGTH) return [text];
672
+
673
+ const parts = [];
674
+ let remaining = text;
675
+
676
+ while (remaining.length > 0) {
677
+ if (remaining.length <= MAX_MESSAGE_LENGTH) {
678
+ parts.push(remaining);
679
+ break;
680
+ }
681
+
682
+ // Try to split at code block boundary
683
+ let splitIndex = remaining.lastIndexOf('\n```', MAX_MESSAGE_LENGTH);
684
+ if (splitIndex > MAX_MESSAGE_LENGTH * 0.3) {
685
+ splitIndex += 1; // Include the newline
686
+ } else {
687
+ // Try newline
688
+ splitIndex = remaining.lastIndexOf('\n', MAX_MESSAGE_LENGTH);
689
+ if (splitIndex < MAX_MESSAGE_LENGTH * 0.3) {
690
+ // Try space
691
+ splitIndex = remaining.lastIndexOf(' ', MAX_MESSAGE_LENGTH);
692
+ if (splitIndex < MAX_MESSAGE_LENGTH * 0.3) {
693
+ // Hard split
694
+ splitIndex = MAX_MESSAGE_LENGTH;
695
+ }
696
+ }
697
+ }
698
+
699
+ parts.push(remaining.slice(0, splitIndex));
700
+ remaining = remaining.slice(splitIndex).trimStart();
701
+ }
702
+
703
+ return parts;
704
+ }
705
+
706
+ // --- Channel Mapping Management ---
707
+
708
+ async assignAgentToChannel(channelKey, agentId) {
709
+ if (!this.channelMappings[channelKey]) {
710
+ this.channelMappings[channelKey] = [];
711
+ }
712
+ if (!this.channelMappings[channelKey].includes(agentId)) {
713
+ this.channelMappings[channelKey].push(agentId);
714
+ }
715
+ await this._saveConfig();
716
+ this.logger?.info('[DiscordService] Agent assigned to channel', { channelKey, agentId });
717
+ }
718
+
719
+ async removeAgentFromChannel(channelKey, agentId) {
720
+ if (this.channelMappings[channelKey]) {
721
+ this.channelMappings[channelKey] = this.channelMappings[channelKey].filter(id => id !== agentId);
722
+ if (this.channelMappings[channelKey].length === 0) {
723
+ delete this.channelMappings[channelKey];
724
+ }
725
+ }
726
+ // Clean sticky if removed agent was sticky
727
+ if (this.stickyAgent.get(channelKey) === agentId) {
728
+ this.stickyAgent.delete(channelKey);
729
+ }
730
+ await this._saveConfig();
731
+ this.logger?.info('[DiscordService] Agent removed from channel', { channelKey, agentId });
732
+ }
733
+
734
+ getChannelMappings() {
735
+ return {
736
+ mappings: this.channelMappings,
737
+ knownGuilds: this.knownGuilds,
738
+ knownChannels: this.knownChannels
739
+ };
740
+ }
741
+
742
+ getAvailableChannels() {
743
+ if (!this.client || this.status !== DISCORD_STATUS.CONNECTED) {
744
+ // Return cached channels if disconnected
745
+ return Object.entries(this.knownChannels).map(([key, ch]) => ({
746
+ key,
747
+ channelId: key.split(':')[1],
748
+ guildId: key.split(':')[0],
749
+ name: ch.name,
750
+ guildName: ch.guildName,
751
+ isThread: ch.isThread || false,
752
+ parentKey: ch.parentKey || null
753
+ }));
754
+ }
755
+
756
+ const channels = [];
757
+ const CT = this._ChannelType;
758
+ for (const [guildId, guild] of this.client.guilds.cache) {
759
+ for (const [channelId, channel] of guild.channels.cache) {
760
+ const isText = channel.type === CT?.GuildText || channel.type === 0;
761
+ const isThread = channel.type === CT?.PublicThread || channel.type === 11 ||
762
+ channel.type === CT?.PrivateThread || channel.type === 12;
763
+ if (isText || isThread) {
764
+ const key = `${guildId}:${channelId}`;
765
+ channels.push({
766
+ key,
767
+ channelId,
768
+ guildId,
769
+ name: channel.name,
770
+ guildName: guild.name,
771
+ isThread,
772
+ parentKey: isThread && channel.parentId ? `${guildId}:${channel.parentId}` : null
773
+ });
774
+ }
775
+ }
776
+ }
777
+ return channels;
778
+ }
779
+ }
780
+
781
+ // Singleton pattern (matches Telegram)
782
+ let instance = null;
783
+
784
+ export function getDiscordService(logger) {
785
+ if (!instance) {
786
+ instance = new DiscordService(logger);
787
+ }
788
+ return instance;
789
+ }
790
+
791
+ export { DiscordService, DISCORD_STATUS };
792
+ export default DiscordService;