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,668 +1,667 @@
1
- /**
2
- * WhatsApp Service - Manages WhatsApp Web connection via whatsapp-web.js
3
- *
4
- * Purpose:
5
- * - Handle WhatsApp pairing/linking via QR code
6
- * - Maintain persistent session state
7
- * - Provide data access APIs for the WhatsApp tool
8
- * - Manage allowed chats/groups access control
9
- */
10
-
11
- import fs from 'fs';
12
- import path from 'path';
13
-
14
- // Lazy-load whatsapp-web.js — loaded on first connect(), not at import time
15
- let Client, LocalAuth, MessageMedia;
16
- async function _ensureWhatsApp() {
17
- if (Client) return;
18
- try {
19
- const pkg = await import('whatsapp-web.js');
20
- const mod = pkg.default || pkg;
21
- Client = mod.Client;
22
- LocalAuth = mod.LocalAuth;
23
- MessageMedia = mod.MessageMedia;
24
- } catch (err) {
25
- throw new Error('whatsapp-web.js is not installed. Install it with: npm install whatsapp-web.js');
26
- }
27
- }
28
- import { getUserDataDir } from '../utilities/userDataDir.js';
29
-
30
- // Connection states
31
- const WA_STATUS = {
32
- DISCONNECTED: 'disconnected',
33
- CONNECTING: 'connecting',
34
- QR_READY: 'qr_ready',
35
- AUTHENTICATING: 'authenticating',
36
- CONNECTED: 'connected',
37
- FAILED: 'failed'
38
- };
39
-
40
- class WhatsAppService {
41
- constructor(logger = null) {
42
- this.logger = logger;
43
- this.client = null;
44
- this.status = WA_STATUS.DISCONNECTED;
45
- this.qrCode = null;
46
- this.currentUser = null;
47
- this.webSocketManager = null;
48
-
49
- // Allowed chats/groups (empty = all allowed)
50
- this.allowedChats = [];
51
-
52
- // Persisted config path
53
- this.dataDir = path.join(getUserDataDir(), 'whatsapp');
54
- this.configPath = path.join(this.dataDir, 'whatsapp-config.json');
55
- this.sessionDir = path.join(this.dataDir, 'session');
56
-
57
- // Ensure directories exist
58
- this._ensureDirs();
59
-
60
- // Load saved config
61
- this._loadConfig();
62
- }
63
-
64
- // ========================
65
- // Lifecycle
66
- // ========================
67
-
68
- /**
69
- * Check if a previous session exists and auto-reconnect if so.
70
- * Called once after the WebSocket manager is wired up.
71
- */
72
- async autoReconnect() {
73
- if (this._hasExistingSession()) {
74
- this.logger?.info('WhatsApp: Found existing session, auto-reconnecting...');
75
- try {
76
- await this.connect();
77
- } catch (e) {
78
- this.logger?.warn('WhatsApp auto-reconnect failed', { error: e.message });
79
- }
80
- }
81
- }
82
-
83
- /**
84
- * Check if a saved session directory exists (non-empty)
85
- */
86
- _hasExistingSession() {
87
- try {
88
- if (!fs.existsSync(this.sessionDir)) return false;
89
- const entries = fs.readdirSync(this.sessionDir);
90
- return entries.length > 0;
91
- } catch {
92
- return false;
93
- }
94
- }
95
-
96
- /**
97
- * Initialize and connect the WhatsApp client
98
- */
99
- async connect() {
100
- try {
101
- await _ensureWhatsApp();
102
- } catch (err) {
103
- return {
104
- success: false,
105
- error: err.message,
106
- status: WA_STATUS.FAILED
107
- };
108
- }
109
-
110
- if (this.status === WA_STATUS.CONNECTED) {
111
- return { success: true, status: this.status, user: this.currentUser };
112
- }
113
-
114
- if (this.status === WA_STATUS.CONNECTING || this.status === WA_STATUS.QR_READY) {
115
- return { success: true, status: this.status, qrCode: this.qrCode };
116
- }
117
-
118
- this.status = WA_STATUS.CONNECTING;
119
- this._broadcast('whatsapp_status', { status: this.status });
120
-
121
- try {
122
- this.client = new Client({
123
- authStrategy: new LocalAuth({ dataPath: this.sessionDir }),
124
- puppeteer: {
125
- headless: true,
126
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
127
- }
128
- });
129
-
130
- this._attachClientEvents();
131
- await this.client.initialize();
132
-
133
- return { success: true, status: this.status, qrCode: this.qrCode };
134
- } catch (error) {
135
- this.status = WA_STATUS.FAILED;
136
- this.logger?.error('WhatsApp connection failed', { error: error.message });
137
- this._broadcast('whatsapp_status', { status: this.status, error: error.message });
138
- return { success: false, error: error.message, status: this.status };
139
- }
140
- }
141
-
142
- /**
143
- * Disconnect the WhatsApp client
144
- */
145
- async disconnect() {
146
- if (!this.client) {
147
- this.status = WA_STATUS.DISCONNECTED;
148
- return { success: true };
149
- }
150
-
151
- try {
152
- await this.client.destroy();
153
- } catch (e) {
154
- this.logger?.warn('WhatsApp disconnect error', { error: e.message });
155
- }
156
-
157
- this.client = null;
158
- this.status = WA_STATUS.DISCONNECTED;
159
- this.qrCode = null;
160
- this.currentUser = null;
161
- this._broadcast('whatsapp_status', { status: this.status });
162
- return { success: true };
163
- }
164
-
165
- /**
166
- * Logout — clears session so re-pairing is required
167
- */
168
- async logout() {
169
- if (this.client) {
170
- try {
171
- await this.client.logout();
172
- } catch (e) {
173
- this.logger?.warn('WhatsApp logout error', { error: e.message });
174
- }
175
- await this.disconnect();
176
- }
177
-
178
- // Remove session files
179
- try {
180
- if (fs.existsSync(this.sessionDir)) {
181
- fs.rmSync(this.sessionDir, { recursive: true, force: true });
182
- }
183
- } catch (e) {
184
- this.logger?.warn('Failed to remove session dir', { error: e.message });
185
- }
186
-
187
- return { success: true };
188
- }
189
-
190
- /**
191
- * Get current connection status
192
- */
193
- getStatus() {
194
- return {
195
- status: this.status,
196
- connected: this.status === WA_STATUS.CONNECTED,
197
- qrCode: this.qrCode,
198
- user: this.currentUser,
199
- allowedChats: this.allowedChats
200
- };
201
- }
202
-
203
- // ========================
204
- // Access Control
205
- // ========================
206
-
207
- /**
208
- * Update allowed chats list
209
- * @param {Array<{id: string, name: string}>} chats
210
- */
211
- setAllowedChats(chats) {
212
- this.allowedChats = chats || [];
213
- this._saveConfig();
214
- return { success: true, allowedChats: this.allowedChats };
215
- }
216
-
217
- /**
218
- * Check if a chat is allowed
219
- */
220
- isChatAllowed(chatId) {
221
- if (this.allowedChats.length === 0) return true; // empty = all allowed
222
- return this.allowedChats.some(c => c.id === chatId);
223
- }
224
-
225
- // ========================
226
- // Data Access (used by WhatsAppTool)
227
- // ========================
228
-
229
- /**
230
- * Resolve a chat identifier — accepts either a WhatsApp ID (e.g. "1234@g.us")
231
- * or a human-readable name (e.g. "Work Group"). Returns the serialized chat ID.
232
- * Throws if no match or multiple ambiguous matches are found.
233
- * @param {string} chatIdOrName
234
- * @returns {Promise<string>} Resolved serialized chat ID
235
- */
236
- async resolveChatId(chatIdOrName) {
237
- if (!chatIdOrName) throw new Error('chatId or chat name is required');
238
-
239
- // If it looks like a WhatsApp ID already, return as-is
240
- if (chatIdOrName.includes('@')) {
241
- return chatIdOrName;
242
- }
243
-
244
- // Otherwise search by name (case-insensitive)
245
- this._ensureConnected();
246
- const allChats = await this.client.getChats();
247
- const needle = chatIdOrName.toLowerCase().trim();
248
-
249
- // Exact match first
250
- const exact = allChats.find(c =>
251
- (c.name || c.id.user || '').toLowerCase() === needle
252
- );
253
- if (exact) return exact.id._serialized;
254
-
255
- // Partial/substring match
256
- const partial = allChats.filter(c =>
257
- (c.name || c.id.user || '').toLowerCase().includes(needle)
258
- );
259
-
260
- if (partial.length === 0) {
261
- throw new Error(`No chat found matching "${chatIdOrName}". Use list-chats to see available chats.`);
262
- }
263
- if (partial.length === 1) {
264
- return partial[0].id._serialized;
265
- }
266
-
267
- // Multiple matches return a helpful error
268
- const matches = partial.slice(0, 5).map(c => `"${c.name || c.id.user}" (${c.id._serialized})`).join(', ');
269
- throw new Error(
270
- `Multiple chats match "${chatIdOrName}": ${matches}. ` +
271
- `Please use a more specific name or the exact chat ID.`
272
- );
273
- }
274
-
275
- /**
276
- * List chats with optional filtering and pagination
277
- * @param {Object} options
278
- * @param {string} options.filter - Filter by name (substring, case-insensitive)
279
- * @param {string} options.type - Filter by type: "group", "individual", or "all" (default)
280
- * @param {number} options.page - Page number, 1-based (default: 1)
281
- * @param {number} options.pageSize - Items per page (default: 20)
282
- * @param {boolean} options.unreadOnly - Only show chats with unread messages
283
- */
284
- async listChats(options = {}) {
285
- this._ensureConnected();
286
- const { filter, type = 'all', page = 1, pageSize = 20, unreadOnly = false } = options;
287
-
288
- const rawChats = await this.client.getChats();
289
-
290
- let chats = rawChats.map(chat => ({
291
- id: chat.id._serialized,
292
- name: chat.name || chat.id.user,
293
- isGroup: chat.isGroup,
294
- unreadCount: chat.unreadCount,
295
- lastMessage: chat.lastMessage ? {
296
- body: chat.lastMessage.body?.substring(0, 200),
297
- timestamp: chat.lastMessage.timestamp,
298
- fromMe: chat.lastMessage.fromMe
299
- } : null,
300
- allowed: this.isChatAllowed(chat.id._serialized)
301
- }));
302
-
303
- // Apply filters
304
- if (type === 'group') {
305
- chats = chats.filter(c => c.isGroup);
306
- } else if (type === 'individual') {
307
- chats = chats.filter(c => !c.isGroup);
308
- }
309
-
310
- if (filter) {
311
- const needle = filter.toLowerCase();
312
- chats = chats.filter(c => c.name.toLowerCase().includes(needle));
313
- }
314
-
315
- if (unreadOnly) {
316
- chats = chats.filter(c => c.unreadCount > 0);
317
- }
318
-
319
- // Pagination
320
- const totalChats = chats.length;
321
- const totalPages = Math.ceil(totalChats / pageSize) || 1;
322
- const safePage = Math.max(1, Math.min(page, totalPages));
323
- const startIdx = (safePage - 1) * pageSize;
324
- const pageChats = chats.slice(startIdx, startIdx + pageSize);
325
-
326
- return {
327
- chats: pageChats,
328
- pagination: {
329
- page: safePage,
330
- pageSize,
331
- totalChats,
332
- totalPages,
333
- hasMore: safePage < totalPages
334
- }
335
- };
336
- }
337
-
338
- /**
339
- * Get messages from a specific chat
340
- * @param {string} chatId
341
- * @param {Object} options - { limit, before, after, searchTerm }
342
- */
343
- async getMessages(chatId, options = {}) {
344
- this._ensureConnected();
345
- chatId = await this.resolveChatId(chatId);
346
- this._ensureChatAllowed(chatId);
347
-
348
- const { limit = 50, before, after, searchTerm } = options;
349
-
350
- const chat = await this.client.getChatById(chatId);
351
- if (!chat) throw new Error(`Chat not found: ${chatId}`);
352
-
353
- // Fetch messages whatsapp-web.js uses fetchMessages with limit
354
- const fetchLimit = searchTerm ? Math.max(limit * 5, 200) : limit;
355
- const messages = await chat.fetchMessages({ limit: fetchLimit });
356
-
357
- let filtered = messages;
358
-
359
- // Date filtering
360
- if (before) {
361
- const beforeTs = new Date(before).getTime() / 1000;
362
- filtered = filtered.filter(m => m.timestamp <= beforeTs);
363
- }
364
- if (after) {
365
- const afterTs = new Date(after).getTime() / 1000;
366
- filtered = filtered.filter(m => m.timestamp >= afterTs);
367
- }
368
-
369
- // Text search
370
- if (searchTerm) {
371
- const term = searchTerm.toLowerCase();
372
- filtered = filtered.filter(m =>
373
- m.body?.toLowerCase().includes(term)
374
- );
375
- }
376
-
377
- // Apply final limit
378
- filtered = filtered.slice(-limit);
379
-
380
- return {
381
- chatId,
382
- chatName: chat.name || chat.id.user,
383
- messageCount: filtered.length,
384
- messages: await Promise.all(filtered.map(m => this._formatMessage(m)))
385
- };
386
- }
387
-
388
- /**
389
- * Search across multiple chats
390
- * @param {string} searchTerm
391
- * @param {Object} options - { chatIds, limit, before, after }
392
- */
393
- async searchMessages(searchTerm, options = {}) {
394
- this._ensureConnected();
395
- if (!searchTerm) throw new Error('searchTerm is required');
396
-
397
- const { chatIds, limit = 30, before, after } = options;
398
-
399
- // Determine which chats to search (resolve names to IDs)
400
- let chatsToSearch;
401
- if (chatIds && chatIds.length > 0) {
402
- chatsToSearch = await Promise.all(chatIds.map(id => this.resolveChatId(id)));
403
- } else if (this.allowedChats.length > 0) {
404
- chatsToSearch = this.allowedChats.map(c => c.id);
405
- } else {
406
- // Search all chats — get up to 20 most recent
407
- const allChats = await this.client.getChats();
408
- chatsToSearch = allChats.slice(0, 20).map(c => c.id._serialized);
409
- }
410
-
411
- const results = [];
412
- for (const chatId of chatsToSearch) {
413
- try {
414
- const chatResults = await this.getMessages(chatId, {
415
- limit: Math.ceil(limit / chatsToSearch.length) || 10,
416
- before,
417
- after,
418
- searchTerm
419
- });
420
- if (chatResults.messages.length > 0) {
421
- results.push(chatResults);
422
- }
423
- } catch (e) {
424
- this.logger?.debug('Search skip chat', { chatId, error: e.message });
425
- }
426
- }
427
-
428
- return {
429
- searchTerm,
430
- totalMatches: results.reduce((s, r) => s + r.messageCount, 0),
431
- chatsSearched: chatsToSearch.length,
432
- results
433
- };
434
- }
435
-
436
- /**
437
- * Get media/file from a message
438
- * @param {string} chatId
439
- * @param {string} messageId - serialized message ID
440
- */
441
- async getMedia(chatId, messageId) {
442
- this._ensureConnected();
443
- chatId = await this.resolveChatId(chatId);
444
- this._ensureChatAllowed(chatId);
445
-
446
- const chat = await this.client.getChatById(chatId);
447
- if (!chat) throw new Error(`Chat not found: ${chatId}`);
448
-
449
- // Fetch a batch of messages to find the target one
450
- const messages = await chat.fetchMessages({ limit: 100 });
451
- const message = messages.find(m => m.id._serialized === messageId);
452
-
453
- if (!message) throw new Error(`Message not found: ${messageId}`);
454
- if (!message.hasMedia) throw new Error('Message has no media');
455
-
456
- const media = await message.downloadMedia();
457
- if (!media) throw new Error('Failed to download media');
458
-
459
- // Save to temp dir
460
- const ext = this._mimeToExt(media.mimetype);
461
- const filename = `wa-media-${Date.now()}${ext}`;
462
- const outputDir = path.join(this.dataDir, 'media');
463
- if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
464
-
465
- const filePath = path.join(outputDir, filename);
466
- fs.writeFileSync(filePath, media.data, 'base64');
467
-
468
- return {
469
- filename,
470
- mimetype: media.mimetype,
471
- filePath,
472
- size: Buffer.from(media.data, 'base64').length
473
- };
474
- }
475
-
476
- /**
477
- * Get chat info (participants, description, etc.)
478
- */
479
- async getChatInfo(chatId) {
480
- this._ensureConnected();
481
- chatId = await this.resolveChatId(chatId);
482
- this._ensureChatAllowed(chatId);
483
-
484
- const chat = await this.client.getChatById(chatId);
485
- if (!chat) throw new Error(`Chat not found: ${chatId}`);
486
-
487
- const info = {
488
- id: chat.id._serialized,
489
- name: chat.name || chat.id.user,
490
- isGroup: chat.isGroup,
491
- unreadCount: chat.unreadCount,
492
- timestamp: chat.timestamp,
493
- muteExpiration: chat.muteExpiration
494
- };
495
-
496
- if (chat.isGroup) {
497
- info.description = chat.groupMetadata?.desc || '';
498
- info.participants = (chat.groupMetadata?.participants || []).map(p => ({
499
- id: p.id._serialized,
500
- isAdmin: p.isAdmin || false,
501
- isSuperAdmin: p.isSuperAdmin || false
502
- }));
503
- info.participantCount = info.participants.length;
504
- }
505
-
506
- return info;
507
- }
508
-
509
- // ========================
510
- // Internal helpers
511
- // ========================
512
-
513
- setWebSocketManager(wsManager) {
514
- this.webSocketManager = wsManager;
515
- }
516
-
517
- _attachClientEvents() {
518
- this.client.on('qr', (qr) => {
519
- this.qrCode = qr;
520
- this.status = WA_STATUS.QR_READY;
521
- this.logger?.info('WhatsApp QR code generated');
522
- this._broadcast('whatsapp_qr', { qrCode: qr });
523
- this._broadcast('whatsapp_status', { status: this.status });
524
- });
525
-
526
- this.client.on('authenticated', () => {
527
- this.status = WA_STATUS.AUTHENTICATING;
528
- this.qrCode = null;
529
- this.logger?.info('WhatsApp authenticated');
530
- this._broadcast('whatsapp_status', { status: this.status });
531
- });
532
-
533
- this.client.on('ready', async () => {
534
- this.status = WA_STATUS.CONNECTED;
535
- this.qrCode = null;
536
-
537
- try {
538
- const info = this.client.info;
539
- this.currentUser = {
540
- id: info.wid._serialized,
541
- name: info.pushname,
542
- phone: info.wid.user
543
- };
544
- } catch (e) {
545
- this.currentUser = { name: 'Connected' };
546
- }
547
-
548
- this.logger?.info('WhatsApp ready', { user: this.currentUser });
549
- this._broadcast('whatsapp_status', { status: this.status, user: this.currentUser });
550
- });
551
-
552
- this.client.on('disconnected', (reason) => {
553
- this.status = WA_STATUS.DISCONNECTED;
554
- this.qrCode = null;
555
- this.currentUser = null;
556
- this.logger?.warn('WhatsApp disconnected', { reason });
557
- this._broadcast('whatsapp_status', { status: this.status, reason });
558
- });
559
-
560
- this.client.on('auth_failure', (msg) => {
561
- this.status = WA_STATUS.FAILED;
562
- this.logger?.error('WhatsApp auth failure', { message: msg });
563
- this._broadcast('whatsapp_status', { status: this.status, error: msg });
564
- });
565
- }
566
-
567
- async _formatMessage(msg) {
568
- const formatted = {
569
- id: msg.id._serialized,
570
- body: msg.body,
571
- timestamp: msg.timestamp,
572
- date: new Date(msg.timestamp * 1000).toISOString(),
573
- fromMe: msg.fromMe,
574
- author: msg.author || msg.from,
575
- type: msg.type, // chat, image, video, audio, document, sticker, etc.
576
- hasMedia: msg.hasMedia,
577
- isForwarded: msg.isForwarded || false
578
- };
579
-
580
- // Include contact name if available
581
- try {
582
- const contact = await msg.getContact();
583
- formatted.authorName = contact.pushname || contact.name || contact.number;
584
- } catch {
585
- formatted.authorName = formatted.author;
586
- }
587
-
588
- // Media metadata (without downloading)
589
- if (msg.hasMedia) {
590
- formatted.mediaType = msg.type;
591
- if (msg._data?.mimetype) formatted.mimetype = msg._data.mimetype;
592
- if (msg._data?.filename) formatted.fileName = msg._data.filename;
593
- if (msg._data?.size) formatted.fileSize = msg._data.size;
594
- formatted.caption = msg._data?.caption || null;
595
- }
596
-
597
- return formatted;
598
- }
599
-
600
- _ensureConnected() {
601
- if (this.status !== WA_STATUS.CONNECTED || !this.client) {
602
- throw new Error('WhatsApp is not connected. Please link your account first in Settings.');
603
- }
604
- }
605
-
606
- _ensureChatAllowed(chatId) {
607
- if (!this.isChatAllowed(chatId)) {
608
- throw new Error(`Access denied: Chat ${chatId} is not in the allowed list. Update allowed chats in Settings.`);
609
- }
610
- }
611
-
612
- _broadcast(type, data) {
613
- if (this.webSocketManager) {
614
- // Broadcast to all sessions
615
- this.webSocketManager.broadcastToSession(null, {
616
- type,
617
- data,
618
- timestamp: new Date().toISOString()
619
- });
620
- }
621
- }
622
-
623
- _ensureDirs() {
624
- for (const dir of [this.dataDir, this.sessionDir]) {
625
- if (!fs.existsSync(dir)) {
626
- fs.mkdirSync(dir, { recursive: true });
627
- }
628
- }
629
- }
630
-
631
- _loadConfig() {
632
- try {
633
- if (fs.existsSync(this.configPath)) {
634
- const raw = fs.readFileSync(this.configPath, 'utf-8');
635
- const config = JSON.parse(raw);
636
- this.allowedChats = config.allowedChats || [];
637
- }
638
- } catch (e) {
639
- this.logger?.warn('Failed to load WhatsApp config', { error: e.message });
640
- }
641
- }
642
-
643
- _saveConfig() {
644
- try {
645
- fs.writeFileSync(this.configPath, JSON.stringify({
646
- allowedChats: this.allowedChats,
647
- updatedAt: new Date().toISOString()
648
- }, null, 2));
649
- } catch (e) {
650
- this.logger?.warn('Failed to save WhatsApp config', { error: e.message });
651
- }
652
- }
653
-
654
- _mimeToExt(mime) {
655
- const map = {
656
- 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp',
657
- 'video/mp4': '.mp4', 'audio/ogg': '.ogg', 'audio/mpeg': '.mp3',
658
- 'application/pdf': '.pdf', 'application/msword': '.doc',
659
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
660
- 'application/vnd.ms-excel': '.xls',
661
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx'
662
- };
663
- return map[mime] || '.bin';
664
- }
665
- }
666
-
667
- export default WhatsAppService;
668
- export { WA_STATUS };
1
+ /**
2
+ * WhatsApp Service - Manages WhatsApp Web connection via whatsapp-web.js
3
+ *
4
+ * Purpose:
5
+ * - Handle WhatsApp pairing/linking via QR code
6
+ * - Maintain persistent session state
7
+ * - Provide data access APIs for the WhatsApp tool
8
+ * - Manage allowed chats/groups access control
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+
14
+ // Lazy-load whatsapp-web.js — loaded on first connect(), not at import time
15
+ let Client, LocalAuth;
16
+ async function _ensureWhatsApp() {
17
+ if (Client) return;
18
+ try {
19
+ const pkg = await import('whatsapp-web.js');
20
+ const mod = pkg.default || pkg;
21
+ Client = mod.Client;
22
+ LocalAuth = mod.LocalAuth;
23
+ } catch (err) {
24
+ throw new Error('whatsapp-web.js is not installed. Install it with: npm install whatsapp-web.js', { cause: err });
25
+ }
26
+ }
27
+ import { getUserDataDir } from '../utilities/userDataDir.js';
28
+
29
+ // Connection states
30
+ const WA_STATUS = {
31
+ DISCONNECTED: 'disconnected',
32
+ CONNECTING: 'connecting',
33
+ QR_READY: 'qr_ready',
34
+ AUTHENTICATING: 'authenticating',
35
+ CONNECTED: 'connected',
36
+ FAILED: 'failed'
37
+ };
38
+
39
+ class WhatsAppService {
40
+ constructor(logger = null) {
41
+ this.logger = logger;
42
+ this.client = null;
43
+ this.status = WA_STATUS.DISCONNECTED;
44
+ this.qrCode = null;
45
+ this.currentUser = null;
46
+ this.webSocketManager = null;
47
+
48
+ // Allowed chats/groups (empty = all allowed)
49
+ this.allowedChats = [];
50
+
51
+ // Persisted config path
52
+ this.dataDir = path.join(getUserDataDir(), 'whatsapp');
53
+ this.configPath = path.join(this.dataDir, 'whatsapp-config.json');
54
+ this.sessionDir = path.join(this.dataDir, 'session');
55
+
56
+ // Ensure directories exist
57
+ this._ensureDirs();
58
+
59
+ // Load saved config
60
+ this._loadConfig();
61
+ }
62
+
63
+ // ========================
64
+ // Lifecycle
65
+ // ========================
66
+
67
+ /**
68
+ * Check if a previous session exists and auto-reconnect if so.
69
+ * Called once after the WebSocket manager is wired up.
70
+ */
71
+ async autoReconnect() {
72
+ if (this._hasExistingSession()) {
73
+ this.logger?.info('WhatsApp: Found existing session, auto-reconnecting...');
74
+ try {
75
+ await this.connect();
76
+ } catch (e) {
77
+ this.logger?.warn('WhatsApp auto-reconnect failed', { error: e.message });
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if a saved session directory exists (non-empty)
84
+ */
85
+ _hasExistingSession() {
86
+ try {
87
+ if (!fs.existsSync(this.sessionDir)) return false;
88
+ const entries = fs.readdirSync(this.sessionDir);
89
+ return entries.length > 0;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Initialize and connect the WhatsApp client
97
+ */
98
+ async connect() {
99
+ try {
100
+ await _ensureWhatsApp();
101
+ } catch (err) {
102
+ return {
103
+ success: false,
104
+ error: err.message,
105
+ status: WA_STATUS.FAILED
106
+ };
107
+ }
108
+
109
+ if (this.status === WA_STATUS.CONNECTED) {
110
+ return { success: true, status: this.status, user: this.currentUser };
111
+ }
112
+
113
+ if (this.status === WA_STATUS.CONNECTING || this.status === WA_STATUS.QR_READY) {
114
+ return { success: true, status: this.status, qrCode: this.qrCode };
115
+ }
116
+
117
+ this.status = WA_STATUS.CONNECTING;
118
+ this._broadcast('whatsapp_status', { status: this.status });
119
+
120
+ try {
121
+ this.client = new Client({
122
+ authStrategy: new LocalAuth({ dataPath: this.sessionDir }),
123
+ puppeteer: {
124
+ headless: true,
125
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
126
+ }
127
+ });
128
+
129
+ this._attachClientEvents();
130
+ await this.client.initialize();
131
+
132
+ return { success: true, status: this.status, qrCode: this.qrCode };
133
+ } catch (error) {
134
+ this.status = WA_STATUS.FAILED;
135
+ this.logger?.error('WhatsApp connection failed', { error: error.message });
136
+ this._broadcast('whatsapp_status', { status: this.status, error: error.message });
137
+ return { success: false, error: error.message, status: this.status };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Disconnect the WhatsApp client
143
+ */
144
+ async disconnect() {
145
+ if (!this.client) {
146
+ this.status = WA_STATUS.DISCONNECTED;
147
+ return { success: true };
148
+ }
149
+
150
+ try {
151
+ await this.client.destroy();
152
+ } catch (e) {
153
+ this.logger?.warn('WhatsApp disconnect error', { error: e.message });
154
+ }
155
+
156
+ this.client = null;
157
+ this.status = WA_STATUS.DISCONNECTED;
158
+ this.qrCode = null;
159
+ this.currentUser = null;
160
+ this._broadcast('whatsapp_status', { status: this.status });
161
+ return { success: true };
162
+ }
163
+
164
+ /**
165
+ * Logout — clears session so re-pairing is required
166
+ */
167
+ async logout() {
168
+ if (this.client) {
169
+ try {
170
+ await this.client.logout();
171
+ } catch (e) {
172
+ this.logger?.warn('WhatsApp logout error', { error: e.message });
173
+ }
174
+ await this.disconnect();
175
+ }
176
+
177
+ // Remove session files
178
+ try {
179
+ if (fs.existsSync(this.sessionDir)) {
180
+ fs.rmSync(this.sessionDir, { recursive: true, force: true });
181
+ }
182
+ } catch (e) {
183
+ this.logger?.warn('Failed to remove session dir', { error: e.message });
184
+ }
185
+
186
+ return { success: true };
187
+ }
188
+
189
+ /**
190
+ * Get current connection status
191
+ */
192
+ getStatus() {
193
+ return {
194
+ status: this.status,
195
+ connected: this.status === WA_STATUS.CONNECTED,
196
+ qrCode: this.qrCode,
197
+ user: this.currentUser,
198
+ allowedChats: this.allowedChats
199
+ };
200
+ }
201
+
202
+ // ========================
203
+ // Access Control
204
+ // ========================
205
+
206
+ /**
207
+ * Update allowed chats list
208
+ * @param {Array<{id: string, name: string}>} chats
209
+ */
210
+ setAllowedChats(chats) {
211
+ this.allowedChats = chats || [];
212
+ this._saveConfig();
213
+ return { success: true, allowedChats: this.allowedChats };
214
+ }
215
+
216
+ /**
217
+ * Check if a chat is allowed
218
+ */
219
+ isChatAllowed(chatId) {
220
+ if (this.allowedChats.length === 0) return true; // empty = all allowed
221
+ return this.allowedChats.some(c => c.id === chatId);
222
+ }
223
+
224
+ // ========================
225
+ // Data Access (used by WhatsAppTool)
226
+ // ========================
227
+
228
+ /**
229
+ * Resolve a chat identifier — accepts either a WhatsApp ID (e.g. "1234@g.us")
230
+ * or a human-readable name (e.g. "Work Group"). Returns the serialized chat ID.
231
+ * Throws if no match or multiple ambiguous matches are found.
232
+ * @param {string} chatIdOrName
233
+ * @returns {Promise<string>} Resolved serialized chat ID
234
+ */
235
+ async resolveChatId(chatIdOrName) {
236
+ if (!chatIdOrName) throw new Error('chatId or chat name is required');
237
+
238
+ // If it looks like a WhatsApp ID already, return as-is
239
+ if (chatIdOrName.includes('@')) {
240
+ return chatIdOrName;
241
+ }
242
+
243
+ // Otherwise search by name (case-insensitive)
244
+ this._ensureConnected();
245
+ const allChats = await this.client.getChats();
246
+ const needle = chatIdOrName.toLowerCase().trim();
247
+
248
+ // Exact match first
249
+ const exact = allChats.find(c =>
250
+ (c.name || c.id.user || '').toLowerCase() === needle
251
+ );
252
+ if (exact) return exact.id._serialized;
253
+
254
+ // Partial/substring match
255
+ const partial = allChats.filter(c =>
256
+ (c.name || c.id.user || '').toLowerCase().includes(needle)
257
+ );
258
+
259
+ if (partial.length === 0) {
260
+ throw new Error(`No chat found matching "${chatIdOrName}". Use list-chats to see available chats.`);
261
+ }
262
+ if (partial.length === 1) {
263
+ return partial[0].id._serialized;
264
+ }
265
+
266
+ // Multiple matches — return a helpful error
267
+ const matches = partial.slice(0, 5).map(c => `"${c.name || c.id.user}" (${c.id._serialized})`).join(', ');
268
+ throw new Error(
269
+ `Multiple chats match "${chatIdOrName}": ${matches}. ` +
270
+ `Please use a more specific name or the exact chat ID.`
271
+ );
272
+ }
273
+
274
+ /**
275
+ * List chats with optional filtering and pagination
276
+ * @param {Object} options
277
+ * @param {string} options.filter - Filter by name (substring, case-insensitive)
278
+ * @param {string} options.type - Filter by type: "group", "individual", or "all" (default)
279
+ * @param {number} options.page - Page number, 1-based (default: 1)
280
+ * @param {number} options.pageSize - Items per page (default: 20)
281
+ * @param {boolean} options.unreadOnly - Only show chats with unread messages
282
+ */
283
+ async listChats(options = {}) {
284
+ this._ensureConnected();
285
+ const { filter, type = 'all', page = 1, pageSize = 20, unreadOnly = false } = options;
286
+
287
+ const rawChats = await this.client.getChats();
288
+
289
+ let chats = rawChats.map(chat => ({
290
+ id: chat.id._serialized,
291
+ name: chat.name || chat.id.user,
292
+ isGroup: chat.isGroup,
293
+ unreadCount: chat.unreadCount,
294
+ lastMessage: chat.lastMessage ? {
295
+ body: chat.lastMessage.body?.substring(0, 200),
296
+ timestamp: chat.lastMessage.timestamp,
297
+ fromMe: chat.lastMessage.fromMe
298
+ } : null,
299
+ allowed: this.isChatAllowed(chat.id._serialized)
300
+ }));
301
+
302
+ // Apply filters
303
+ if (type === 'group') {
304
+ chats = chats.filter(c => c.isGroup);
305
+ } else if (type === 'individual') {
306
+ chats = chats.filter(c => !c.isGroup);
307
+ }
308
+
309
+ if (filter) {
310
+ const needle = filter.toLowerCase();
311
+ chats = chats.filter(c => c.name.toLowerCase().includes(needle));
312
+ }
313
+
314
+ if (unreadOnly) {
315
+ chats = chats.filter(c => c.unreadCount > 0);
316
+ }
317
+
318
+ // Pagination
319
+ const totalChats = chats.length;
320
+ const totalPages = Math.ceil(totalChats / pageSize) || 1;
321
+ const safePage = Math.max(1, Math.min(page, totalPages));
322
+ const startIdx = (safePage - 1) * pageSize;
323
+ const pageChats = chats.slice(startIdx, startIdx + pageSize);
324
+
325
+ return {
326
+ chats: pageChats,
327
+ pagination: {
328
+ page: safePage,
329
+ pageSize,
330
+ totalChats,
331
+ totalPages,
332
+ hasMore: safePage < totalPages
333
+ }
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Get messages from a specific chat
339
+ * @param {string} chatId
340
+ * @param {Object} options - { limit, before, after, searchTerm }
341
+ */
342
+ async getMessages(chatId, options = {}) {
343
+ this._ensureConnected();
344
+ chatId = await this.resolveChatId(chatId);
345
+ this._ensureChatAllowed(chatId);
346
+
347
+ const { limit = 50, before, after, searchTerm } = options;
348
+
349
+ const chat = await this.client.getChatById(chatId);
350
+ if (!chat) throw new Error(`Chat not found: ${chatId}`);
351
+
352
+ // Fetch messages — whatsapp-web.js uses fetchMessages with limit
353
+ const fetchLimit = searchTerm ? Math.max(limit * 5, 200) : limit;
354
+ const messages = await chat.fetchMessages({ limit: fetchLimit });
355
+
356
+ let filtered = messages;
357
+
358
+ // Date filtering
359
+ if (before) {
360
+ const beforeTs = new Date(before).getTime() / 1000;
361
+ filtered = filtered.filter(m => m.timestamp <= beforeTs);
362
+ }
363
+ if (after) {
364
+ const afterTs = new Date(after).getTime() / 1000;
365
+ filtered = filtered.filter(m => m.timestamp >= afterTs);
366
+ }
367
+
368
+ // Text search
369
+ if (searchTerm) {
370
+ const term = searchTerm.toLowerCase();
371
+ filtered = filtered.filter(m =>
372
+ m.body?.toLowerCase().includes(term)
373
+ );
374
+ }
375
+
376
+ // Apply final limit
377
+ filtered = filtered.slice(-limit);
378
+
379
+ return {
380
+ chatId,
381
+ chatName: chat.name || chat.id.user,
382
+ messageCount: filtered.length,
383
+ messages: await Promise.all(filtered.map(m => this._formatMessage(m)))
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Search across multiple chats
389
+ * @param {string} searchTerm
390
+ * @param {Object} options - { chatIds, limit, before, after }
391
+ */
392
+ async searchMessages(searchTerm, options = {}) {
393
+ this._ensureConnected();
394
+ if (!searchTerm) throw new Error('searchTerm is required');
395
+
396
+ const { chatIds, limit = 30, before, after } = options;
397
+
398
+ // Determine which chats to search (resolve names to IDs)
399
+ let chatsToSearch;
400
+ if (chatIds && chatIds.length > 0) {
401
+ chatsToSearch = await Promise.all(chatIds.map(id => this.resolveChatId(id)));
402
+ } else if (this.allowedChats.length > 0) {
403
+ chatsToSearch = this.allowedChats.map(c => c.id);
404
+ } else {
405
+ // Search all chats — get up to 20 most recent
406
+ const allChats = await this.client.getChats();
407
+ chatsToSearch = allChats.slice(0, 20).map(c => c.id._serialized);
408
+ }
409
+
410
+ const results = [];
411
+ for (const chatId of chatsToSearch) {
412
+ try {
413
+ const chatResults = await this.getMessages(chatId, {
414
+ limit: Math.ceil(limit / chatsToSearch.length) || 10,
415
+ before,
416
+ after,
417
+ searchTerm
418
+ });
419
+ if (chatResults.messages.length > 0) {
420
+ results.push(chatResults);
421
+ }
422
+ } catch (e) {
423
+ this.logger?.debug('Search skip chat', { chatId, error: e.message });
424
+ }
425
+ }
426
+
427
+ return {
428
+ searchTerm,
429
+ totalMatches: results.reduce((s, r) => s + r.messageCount, 0),
430
+ chatsSearched: chatsToSearch.length,
431
+ results
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Get media/file from a message
437
+ * @param {string} chatId
438
+ * @param {string} messageId - serialized message ID
439
+ */
440
+ async getMedia(chatId, messageId) {
441
+ this._ensureConnected();
442
+ chatId = await this.resolveChatId(chatId);
443
+ this._ensureChatAllowed(chatId);
444
+
445
+ const chat = await this.client.getChatById(chatId);
446
+ if (!chat) throw new Error(`Chat not found: ${chatId}`);
447
+
448
+ // Fetch a batch of messages to find the target one
449
+ const messages = await chat.fetchMessages({ limit: 100 });
450
+ const message = messages.find(m => m.id._serialized === messageId);
451
+
452
+ if (!message) throw new Error(`Message not found: ${messageId}`);
453
+ if (!message.hasMedia) throw new Error('Message has no media');
454
+
455
+ const media = await message.downloadMedia();
456
+ if (!media) throw new Error('Failed to download media');
457
+
458
+ // Save to temp dir
459
+ const ext = this._mimeToExt(media.mimetype);
460
+ const filename = `wa-media-${Date.now()}${ext}`;
461
+ const outputDir = path.join(this.dataDir, 'media');
462
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
463
+
464
+ const filePath = path.join(outputDir, filename);
465
+ fs.writeFileSync(filePath, media.data, 'base64');
466
+
467
+ return {
468
+ filename,
469
+ mimetype: media.mimetype,
470
+ filePath,
471
+ size: Buffer.from(media.data, 'base64').length
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Get chat info (participants, description, etc.)
477
+ */
478
+ async getChatInfo(chatId) {
479
+ this._ensureConnected();
480
+ chatId = await this.resolveChatId(chatId);
481
+ this._ensureChatAllowed(chatId);
482
+
483
+ const chat = await this.client.getChatById(chatId);
484
+ if (!chat) throw new Error(`Chat not found: ${chatId}`);
485
+
486
+ const info = {
487
+ id: chat.id._serialized,
488
+ name: chat.name || chat.id.user,
489
+ isGroup: chat.isGroup,
490
+ unreadCount: chat.unreadCount,
491
+ timestamp: chat.timestamp,
492
+ muteExpiration: chat.muteExpiration
493
+ };
494
+
495
+ if (chat.isGroup) {
496
+ info.description = chat.groupMetadata?.desc || '';
497
+ info.participants = (chat.groupMetadata?.participants || []).map(p => ({
498
+ id: p.id._serialized,
499
+ isAdmin: p.isAdmin || false,
500
+ isSuperAdmin: p.isSuperAdmin || false
501
+ }));
502
+ info.participantCount = info.participants.length;
503
+ }
504
+
505
+ return info;
506
+ }
507
+
508
+ // ========================
509
+ // Internal helpers
510
+ // ========================
511
+
512
+ setWebSocketManager(wsManager) {
513
+ this.webSocketManager = wsManager;
514
+ }
515
+
516
+ _attachClientEvents() {
517
+ this.client.on('qr', (qr) => {
518
+ this.qrCode = qr;
519
+ this.status = WA_STATUS.QR_READY;
520
+ this.logger?.info('WhatsApp QR code generated');
521
+ this._broadcast('whatsapp_qr', { qrCode: qr });
522
+ this._broadcast('whatsapp_status', { status: this.status });
523
+ });
524
+
525
+ this.client.on('authenticated', () => {
526
+ this.status = WA_STATUS.AUTHENTICATING;
527
+ this.qrCode = null;
528
+ this.logger?.info('WhatsApp authenticated');
529
+ this._broadcast('whatsapp_status', { status: this.status });
530
+ });
531
+
532
+ this.client.on('ready', async () => {
533
+ this.status = WA_STATUS.CONNECTED;
534
+ this.qrCode = null;
535
+
536
+ try {
537
+ const info = this.client.info;
538
+ this.currentUser = {
539
+ id: info.wid._serialized,
540
+ name: info.pushname,
541
+ phone: info.wid.user
542
+ };
543
+ } catch {
544
+ this.currentUser = { name: 'Connected' };
545
+ }
546
+
547
+ this.logger?.info('WhatsApp ready', { user: this.currentUser });
548
+ this._broadcast('whatsapp_status', { status: this.status, user: this.currentUser });
549
+ });
550
+
551
+ this.client.on('disconnected', (reason) => {
552
+ this.status = WA_STATUS.DISCONNECTED;
553
+ this.qrCode = null;
554
+ this.currentUser = null;
555
+ this.logger?.warn('WhatsApp disconnected', { reason });
556
+ this._broadcast('whatsapp_status', { status: this.status, reason });
557
+ });
558
+
559
+ this.client.on('auth_failure', (msg) => {
560
+ this.status = WA_STATUS.FAILED;
561
+ this.logger?.error('WhatsApp auth failure', { message: msg });
562
+ this._broadcast('whatsapp_status', { status: this.status, error: msg });
563
+ });
564
+ }
565
+
566
+ async _formatMessage(msg) {
567
+ const formatted = {
568
+ id: msg.id._serialized,
569
+ body: msg.body,
570
+ timestamp: msg.timestamp,
571
+ date: new Date(msg.timestamp * 1000).toISOString(),
572
+ fromMe: msg.fromMe,
573
+ author: msg.author || msg.from,
574
+ type: msg.type, // chat, image, video, audio, document, sticker, etc.
575
+ hasMedia: msg.hasMedia,
576
+ isForwarded: msg.isForwarded || false
577
+ };
578
+
579
+ // Include contact name if available
580
+ try {
581
+ const contact = await msg.getContact();
582
+ formatted.authorName = contact.pushname || contact.name || contact.number;
583
+ } catch {
584
+ formatted.authorName = formatted.author;
585
+ }
586
+
587
+ // Media metadata (without downloading)
588
+ if (msg.hasMedia) {
589
+ formatted.mediaType = msg.type;
590
+ if (msg._data?.mimetype) formatted.mimetype = msg._data.mimetype;
591
+ if (msg._data?.filename) formatted.fileName = msg._data.filename;
592
+ if (msg._data?.size) formatted.fileSize = msg._data.size;
593
+ formatted.caption = msg._data?.caption || null;
594
+ }
595
+
596
+ return formatted;
597
+ }
598
+
599
+ _ensureConnected() {
600
+ if (this.status !== WA_STATUS.CONNECTED || !this.client) {
601
+ throw new Error('WhatsApp is not connected. Please link your account first in Settings.');
602
+ }
603
+ }
604
+
605
+ _ensureChatAllowed(chatId) {
606
+ if (!this.isChatAllowed(chatId)) {
607
+ throw new Error(`Access denied: Chat ${chatId} is not in the allowed list. Update allowed chats in Settings.`);
608
+ }
609
+ }
610
+
611
+ _broadcast(type, data) {
612
+ if (this.webSocketManager) {
613
+ // Broadcast to all sessions
614
+ this.webSocketManager.broadcastToSession(null, {
615
+ type,
616
+ data,
617
+ timestamp: new Date().toISOString()
618
+ });
619
+ }
620
+ }
621
+
622
+ _ensureDirs() {
623
+ for (const dir of [this.dataDir, this.sessionDir]) {
624
+ if (!fs.existsSync(dir)) {
625
+ fs.mkdirSync(dir, { recursive: true });
626
+ }
627
+ }
628
+ }
629
+
630
+ _loadConfig() {
631
+ try {
632
+ if (fs.existsSync(this.configPath)) {
633
+ const raw = fs.readFileSync(this.configPath, 'utf-8');
634
+ const config = JSON.parse(raw);
635
+ this.allowedChats = config.allowedChats || [];
636
+ }
637
+ } catch (e) {
638
+ this.logger?.warn('Failed to load WhatsApp config', { error: e.message });
639
+ }
640
+ }
641
+
642
+ _saveConfig() {
643
+ try {
644
+ fs.writeFileSync(this.configPath, JSON.stringify({
645
+ allowedChats: this.allowedChats,
646
+ updatedAt: new Date().toISOString()
647
+ }, null, 2));
648
+ } catch (e) {
649
+ this.logger?.warn('Failed to save WhatsApp config', { error: e.message });
650
+ }
651
+ }
652
+
653
+ _mimeToExt(mime) {
654
+ const map = {
655
+ 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp',
656
+ 'video/mp4': '.mp4', 'audio/ogg': '.ogg', 'audio/mpeg': '.mp3',
657
+ 'application/pdf': '.pdf', 'application/msword': '.doc',
658
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
659
+ 'application/vnd.ms-excel': '.xls',
660
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx'
661
+ };
662
+ return map[mime] || '.bin';
663
+ }
664
+ }
665
+
666
+ export default WhatsAppService;
667
+ export { WA_STATUS };