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,1033 +1,1033 @@
1
- /**
2
- * Visual Editor Bridge Service
3
- *
4
- * Manages visual editor instances per agent, enabling users to interact
5
- * with their web applications visually and give the AI agent pointers
6
- * to code parts of interest.
7
- *
8
- * Key responsibilities:
9
- * - Instance registry (one editor per agent)
10
- * - Visual context storage (element selections)
11
- * - Lifecycle management (create/stop/cleanup)
12
- * - Multi-instance coordination
13
- * - WebSocket connection to visual editor (Phase 3)
14
- *
15
- * @see VISUAL-EDITOR-INTEGRATION-PLAN.md for full architecture
16
- */
17
-
18
- import { EventEmitter } from 'events';
19
- import WebSocket from 'ws';
20
- import { getVisualEditorPort } from './visualEditorServer.js';
21
-
22
- // Configuration defaults - port can be configured via config file, env var, or fallback
23
- const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
24
- const DEFAULT_MAX_INSTANCES = 3;
25
- const DEFAULT_RECONNECT_INTERVAL_MS = 3000;
26
- const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
27
-
28
- /**
29
- * Get the WebSocket URL for the visual editor (computed at runtime)
30
- * Uses getVisualEditorPort which respects config file and env var settings
31
- */
32
- function getVisualEditorWsUrl() {
33
- const port = getVisualEditorPort();
34
- return `ws://localhost:${port}/ws`;
35
- }
36
-
37
- /**
38
- * WebSocket message types from visual editor
39
- */
40
- export const MessageTypes = {
41
- // Incoming from editor
42
- ELEMENT_SELECTED: 'element-selected',
43
- FILE_CHANGED: 'file-changed',
44
- EDITOR_READY: 'editor-ready',
45
- ERROR: 'error',
46
- PONG: 'pong',
47
-
48
- // Outgoing to editor
49
- HIGHLIGHT: 'highlight',
50
- SCROLL_TO: 'scroll-to',
51
- RELOAD: 'reload',
52
- SET_MODE: 'set-mode',
53
- PING: 'ping',
54
- SUBSCRIBE: 'subscribe',
55
- UNSUBSCRIBE: 'unsubscribe'
56
- };
57
-
58
- /**
59
- * Instance status enum
60
- */
61
- export const InstanceStatus = {
62
- INITIALIZED: 'initialized',
63
- CONNECTING: 'connecting',
64
- READY: 'ready',
65
- ERROR: 'error',
66
- STOPPED: 'stopped'
67
- };
68
-
69
- /**
70
- * Visual Editor Bridge Service
71
- * Manages visual editor instances and coordinates communication
72
- */
73
- class VisualEditorBridge extends EventEmitter {
74
- /**
75
- * @param {Object} config - Configuration options
76
- * @param {number} config.maxInstances - Maximum concurrent editors (default: 3)
77
- * @param {number} config.idleTimeoutMs - Idle timeout in ms (default: 10 min)
78
- * @param {string} config.visualEditorUrl - WebSocket URL for visual editor
79
- * @param {number} config.reconnectIntervalMs - Reconnection interval
80
- * @param {number} config.maxReconnectAttempts - Max reconnection attempts
81
- * @param {Object} config.logger - Logger instance
82
- */
83
- constructor(config = {}) {
84
- super();
85
-
86
- // Instance registry: agentId → InstanceRecord
87
- this.instances = new Map();
88
-
89
- // Configuration
90
- this.maxInstances = config.maxInstances || DEFAULT_MAX_INSTANCES;
91
- this.idleTimeoutMs = config.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
92
- this.visualEditorUrl = config.visualEditorUrl || getVisualEditorWsUrl();
93
- this.reconnectIntervalMs = config.reconnectIntervalMs || DEFAULT_RECONNECT_INTERVAL_MS;
94
- this.maxReconnectAttempts = config.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS;
95
- this.logger = config.logger || console;
96
- this.enabled = config.enabled !== false;
97
-
98
- // Cleanup interval for orphaned instances
99
- this.cleanupInterval = null;
100
- if (this.enabled) {
101
- this.cleanupInterval = setInterval(() => this._cleanupIdle(), 60000);
102
- }
103
-
104
- this.logger.info?.('[VisualEditorBridge] Initialized', {
105
- maxInstances: this.maxInstances,
106
- idleTimeoutMs: this.idleTimeoutMs,
107
- visualEditorUrl: this.visualEditorUrl,
108
- enabled: this.enabled
109
- }) || this.logger.log('[VisualEditorBridge] Initialized');
110
- }
111
-
112
- /**
113
- * Check if bridge is enabled
114
- * @returns {boolean}
115
- */
116
- isEnabled() {
117
- return this.enabled;
118
- }
119
-
120
- /**
121
- * Get or create instance for agent
122
- * @param {string} agentId - Agent identifier
123
- * @param {Object} options - Instance options
124
- * @param {string} options.projectRoot - Project root directory
125
- * @param {string} options.appUrl - User's app URL to proxy
126
- * @returns {Object} Instance record
127
- * @throws {Error} If max instances reached
128
- */
129
- async getInstance(agentId, options = {}) {
130
- if (!agentId) {
131
- throw new Error('agentId is required');
132
- }
133
-
134
- // Return existing instance
135
- if (this.instances.has(agentId)) {
136
- const instance = this.instances.get(agentId);
137
- instance.lastActivity = Date.now();
138
- this._resetIdleTimer(agentId);
139
-
140
- // Update options if provided
141
- if (options.projectRoot) instance.projectRoot = options.projectRoot;
142
- if (options.appUrl) instance.appUrl = options.appUrl;
143
-
144
- return instance;
145
- }
146
-
147
- // Check instance limit
148
- if (this.instances.size >= this.maxInstances) {
149
- // Try to evict oldest idle instance
150
- const evicted = this._evictOldestIdle();
151
- if (!evicted) {
152
- throw new Error(
153
- `Maximum visual editor instances (${this.maxInstances}) reached. ` +
154
- `Stop an existing editor first.`
155
- );
156
- }
157
- }
158
-
159
- // Create new instance record
160
- const instance = {
161
- agentId,
162
- projectRoot: options.projectRoot || null,
163
- appUrl: options.appUrl || null,
164
- status: InstanceStatus.INITIALIZED,
165
- wsConnection: null,
166
- editorUrl: null,
167
- lastActivity: Date.now(),
168
- createdAt: Date.now(),
169
- uiSubscribers: new Set(),
170
- visualContext: null,
171
- idleTimer: null,
172
- error: null,
173
- // WebSocket connection state (Phase 3)
174
- reconnectAttempts: 0,
175
- reconnectTimer: null,
176
- pingInterval: null,
177
- lastPong: null,
178
- isConnecting: false
179
- };
180
-
181
- this.instances.set(agentId, instance);
182
- this._resetIdleTimer(agentId);
183
-
184
- this.logger.info?.(`[VisualEditorBridge] Created instance for agent: ${agentId}`, {
185
- projectRoot: instance.projectRoot,
186
- appUrl: instance.appUrl
187
- }) || this.logger.log(`[VisualEditorBridge] Created instance: ${agentId}`);
188
-
189
- this.emit('instance-created', { agentId, instance: this._sanitizeInstance(instance) });
190
-
191
- return instance;
192
- }
193
-
194
- /**
195
- * Check if instance exists for agent
196
- * @param {string} agentId - Agent identifier
197
- * @returns {boolean}
198
- */
199
- hasInstance(agentId) {
200
- return this.instances.has(agentId);
201
- }
202
-
203
- /**
204
- * Store visual context (element selection) for agent
205
- * @param {string} agentId - Agent identifier
206
- * @param {Object} elementReference - Element reference from visual editor
207
- * @returns {boolean} Success
208
- */
209
- setVisualContext(agentId, elementReference) {
210
- const instance = this.instances.get(agentId);
211
- if (!instance) {
212
- this.logger.warn?.(`[VisualEditorBridge] No instance for agent: ${agentId}`) ||
213
- this.logger.log(`[VisualEditorBridge] No instance: ${agentId}`);
214
- return false;
215
- }
216
-
217
- instance.visualContext = {
218
- ...elementReference,
219
- receivedAt: new Date().toISOString()
220
- };
221
- instance.lastActivity = Date.now();
222
- this._resetIdleTimer(agentId);
223
-
224
- this.logger.info?.(`[VisualEditorBridge] Visual context set for agent: ${agentId}`, {
225
- selector: elementReference.selector,
226
- sourceFile: elementReference.sourceHint?.file
227
- }) || this.logger.log(`[VisualEditorBridge] Context set: ${agentId}`);
228
-
229
- this.emit('visual-context-updated', {
230
- agentId,
231
- context: instance.visualContext
232
- });
233
-
234
- return true;
235
- }
236
-
237
- /**
238
- * Get visual context for agent
239
- * @param {string} agentId - Agent identifier
240
- * @returns {Object|null} Visual context or null
241
- */
242
- getVisualContext(agentId) {
243
- const instance = this.instances.get(agentId);
244
- return instance?.visualContext || null;
245
- }
246
-
247
- /**
248
- * Clear visual context for agent
249
- * @param {string} agentId - Agent identifier
250
- * @returns {boolean} Success
251
- */
252
- clearVisualContext(agentId) {
253
- const instance = this.instances.get(agentId);
254
- if (instance && instance.visualContext) {
255
- instance.visualContext = null;
256
- this.emit('visual-context-cleared', { agentId });
257
- return true;
258
- }
259
- return false;
260
- }
261
-
262
- /**
263
- * Get instance status
264
- * @param {string} agentId - Agent identifier
265
- * @returns {Object} Status object
266
- */
267
- getStatus(agentId) {
268
- const instance = this.instances.get(agentId);
269
- if (!instance) {
270
- return {
271
- exists: false,
272
- agentId
273
- };
274
- }
275
-
276
- return {
277
- exists: true,
278
- agentId: instance.agentId,
279
- status: instance.status,
280
- projectRoot: instance.projectRoot,
281
- appUrl: instance.appUrl,
282
- editorUrl: instance.editorUrl,
283
- hasVisualContext: !!instance.visualContext,
284
- visualContext: instance.visualContext,
285
- subscriberCount: instance.uiSubscribers.size,
286
- createdAt: instance.createdAt,
287
- lastActivity: instance.lastActivity,
288
- idleMs: Date.now() - instance.lastActivity,
289
- error: instance.error
290
- };
291
- }
292
-
293
- /**
294
- * Update instance status
295
- * @param {string} agentId - Agent identifier
296
- * @param {string} status - New status
297
- * @param {Object} extra - Additional fields to update
298
- * @returns {boolean} Success
299
- */
300
- updateStatus(agentId, status, extra = {}) {
301
- const instance = this.instances.get(agentId);
302
- if (!instance) {
303
- return false;
304
- }
305
-
306
- instance.status = status;
307
- instance.lastActivity = Date.now();
308
-
309
- if (extra.editorUrl) instance.editorUrl = extra.editorUrl;
310
- if (extra.wsConnection) instance.wsConnection = extra.wsConnection;
311
- if (extra.error) instance.error = extra.error;
312
-
313
- this.emit('instance-status-changed', {
314
- agentId,
315
- status,
316
- ...extra
317
- });
318
-
319
- return true;
320
- }
321
-
322
- /**
323
- * Add UI subscriber to instance
324
- * @param {string} agentId - Agent identifier
325
- * @param {string} connectionId - UI connection identifier
326
- * @returns {boolean} Success
327
- */
328
- addSubscriber(agentId, connectionId) {
329
- const instance = this.instances.get(agentId);
330
- if (!instance) {
331
- return false;
332
- }
333
-
334
- instance.uiSubscribers.add(connectionId);
335
- instance.lastActivity = Date.now();
336
- return true;
337
- }
338
-
339
- /**
340
- * Remove UI subscriber from instance
341
- * @param {string} agentId - Agent identifier
342
- * @param {string} connectionId - UI connection identifier
343
- * @returns {boolean} Success
344
- */
345
- removeSubscriber(agentId, connectionId) {
346
- const instance = this.instances.get(agentId);
347
- if (!instance) {
348
- return false;
349
- }
350
-
351
- return instance.uiSubscribers.delete(connectionId);
352
- }
353
-
354
- /**
355
- * Stop and remove instance for agent
356
- * @param {string} agentId - Agent identifier
357
- * @returns {boolean} Success
358
- */
359
- async stopInstance(agentId) {
360
- const instance = this.instances.get(agentId);
361
- if (!instance) {
362
- return false;
363
- }
364
-
365
- // Mark as stopped to prevent reconnection attempts
366
- instance.status = InstanceStatus.STOPPED;
367
-
368
- // Clear idle timer
369
- if (instance.idleTimer) {
370
- clearTimeout(instance.idleTimer);
371
- instance.idleTimer = null;
372
- }
373
-
374
- // Cleanup WebSocket connection (including ping interval and reconnect timer)
375
- this._cleanupConnection(agentId);
376
-
377
- // Remove from registry
378
- this.instances.delete(agentId);
379
-
380
- this.logger.info?.(`[VisualEditorBridge] Stopped instance for agent: ${agentId}`) ||
381
- this.logger.log(`[VisualEditorBridge] Stopped: ${agentId}`);
382
-
383
- this.emit('instance-stopped', { agentId });
384
-
385
- return true;
386
- }
387
-
388
- /**
389
- * Handle agent deletion - cleanup instance
390
- * @param {string} agentId - Agent identifier
391
- * @returns {boolean} Success
392
- */
393
- onAgentDeleted(agentId) {
394
- this.logger.info?.(`[VisualEditorBridge] Agent deleted, cleaning up: ${agentId}`);
395
- return this.stopInstance(agentId);
396
- }
397
-
398
- /**
399
- * Handle agent unload - cleanup instance
400
- * @param {string} agentId - Agent identifier
401
- * @returns {boolean} Success
402
- */
403
- onAgentUnloaded(agentId) {
404
- this.logger.info?.(`[VisualEditorBridge] Agent unloaded, cleaning up: ${agentId}`);
405
- return this.stopInstance(agentId);
406
- }
407
-
408
- /**
409
- * Check if project is used by another agent
410
- * @param {string} agentId - Current agent identifier
411
- * @param {string} projectRoot - Project root to check
412
- * @returns {Object} Collision info
413
- */
414
- checkProjectCollision(agentId, projectRoot) {
415
- if (!projectRoot) {
416
- return { collision: false };
417
- }
418
-
419
- for (const [otherId, instance] of this.instances) {
420
- if (otherId !== agentId && instance.projectRoot === projectRoot) {
421
- return {
422
- collision: true,
423
- otherAgentId: otherId,
424
- message: `Project "${projectRoot}" is already being edited by agent "${otherId}"`
425
- };
426
- }
427
- }
428
- return { collision: false };
429
- }
430
-
431
- /**
432
- * List all instances
433
- * @returns {Array} Array of instance info objects
434
- */
435
- listInstances() {
436
- return Array.from(this.instances.entries()).map(([agentId, instance]) => ({
437
- agentId,
438
- status: instance.status,
439
- projectRoot: instance.projectRoot,
440
- appUrl: instance.appUrl,
441
- hasContext: !!instance.visualContext,
442
- subscriberCount: instance.uiSubscribers.size,
443
- createdAt: instance.createdAt,
444
- lastActivity: instance.lastActivity,
445
- idleMs: Date.now() - instance.lastActivity
446
- }));
447
- }
448
-
449
- /**
450
- * Get count of active instances
451
- * @returns {number}
452
- */
453
- getInstanceCount() {
454
- return this.instances.size;
455
- }
456
-
457
- /**
458
- * Touch instance to reset idle timer
459
- * @param {string} agentId - Agent identifier
460
- */
461
- touchInstance(agentId) {
462
- const instance = this.instances.get(agentId);
463
- if (instance) {
464
- instance.lastActivity = Date.now();
465
- this._resetIdleTimer(agentId);
466
- }
467
- }
468
-
469
- // === WebSocket Methods (Phase 3) ===
470
-
471
- /**
472
- * Connect to visual editor WebSocket
473
- * @param {string} agentId - Agent identifier
474
- * @param {Object} options - Connection options
475
- * @param {string} options.editorUrl - Override editor URL
476
- * @returns {Promise<boolean>} Connection success
477
- */
478
- async connectToEditor(agentId, options = {}) {
479
- const instance = this.instances.get(agentId);
480
- if (!instance) {
481
- throw new Error(`No instance for agent: ${agentId}`);
482
- }
483
-
484
- // Already connected or connecting
485
- if (instance.wsConnection?.readyState === WebSocket.OPEN) {
486
- return true;
487
- }
488
-
489
- if (instance.isConnecting) {
490
- return false;
491
- }
492
-
493
- instance.isConnecting = true;
494
- instance.status = InstanceStatus.CONNECTING;
495
- this.emit('instance-status-changed', { agentId, status: InstanceStatus.CONNECTING });
496
-
497
- const editorUrl = options.editorUrl || this.visualEditorUrl;
498
-
499
- return new Promise((resolve) => {
500
- try {
501
- this.logger.info?.(`[VisualEditorBridge] Connecting to editor for agent: ${agentId}`, {
502
- url: editorUrl
503
- });
504
-
505
- const ws = new WebSocket(editorUrl);
506
-
507
- ws.on('open', () => {
508
- instance.wsConnection = ws;
509
- instance.status = InstanceStatus.READY;
510
- instance.editorUrl = editorUrl;
511
- instance.isConnecting = false;
512
- instance.reconnectAttempts = 0;
513
- instance.error = null;
514
- instance.lastActivity = Date.now();
515
-
516
- this.logger.info?.(`[VisualEditorBridge] Connected to editor for agent: ${agentId}`);
517
-
518
- // Start heartbeat
519
- this._startPingInterval(agentId);
520
-
521
- // Subscribe to editor events for this agent
522
- this.sendCommand(agentId, MessageTypes.SUBSCRIBE, {
523
- agentId,
524
- projectRoot: instance.projectRoot,
525
- appUrl: instance.appUrl
526
- });
527
-
528
- this.emit('editor-connected', { agentId, editorUrl });
529
- this.emit('instance-status-changed', { agentId, status: InstanceStatus.READY });
530
-
531
- resolve(true);
532
- });
533
-
534
- ws.on('message', (data) => {
535
- try {
536
- const message = JSON.parse(data.toString());
537
- this._handleEditorMessage(agentId, message);
538
- } catch (err) {
539
- this.logger.warn?.(`[VisualEditorBridge] Invalid message from editor: ${err.message}`);
540
- }
541
- });
542
-
543
- ws.on('close', (code, reason) => {
544
- this.logger.info?.(`[VisualEditorBridge] Editor connection closed for agent: ${agentId}`, {
545
- code,
546
- reason: reason?.toString()
547
- });
548
-
549
- this._cleanupConnection(agentId);
550
-
551
- // Schedule reconnect if instance still exists and wasn't manually stopped
552
- if (this.instances.has(agentId) && instance.status !== InstanceStatus.STOPPED) {
553
- this._scheduleReconnect(agentId);
554
- }
555
- });
556
-
557
- ws.on('error', (error) => {
558
- this.logger.error?.(`[VisualEditorBridge] Editor connection error for agent: ${agentId}`, {
559
- error: error.message
560
- });
561
-
562
- instance.error = error.message;
563
- instance.isConnecting = false;
564
-
565
- if (instance.status === InstanceStatus.CONNECTING) {
566
- instance.status = InstanceStatus.ERROR;
567
- this.emit('instance-status-changed', { agentId, status: InstanceStatus.ERROR, error: error.message });
568
- }
569
-
570
- this.emit('editor-error', { agentId, error: error.message });
571
- resolve(false);
572
- });
573
-
574
- } catch (error) {
575
- instance.isConnecting = false;
576
- instance.status = InstanceStatus.ERROR;
577
- instance.error = error.message;
578
- this.logger.error?.(`[VisualEditorBridge] Failed to create WebSocket: ${error.message}`);
579
- resolve(false);
580
- }
581
- });
582
- }
583
-
584
- /**
585
- * Disconnect from visual editor
586
- * @param {string} agentId - Agent identifier
587
- * @returns {boolean} Success
588
- */
589
- disconnectFromEditor(agentId) {
590
- const instance = this.instances.get(agentId);
591
- if (!instance) {
592
- return false;
593
- }
594
-
595
- // Send unsubscribe before closing
596
- if (instance.wsConnection?.readyState === WebSocket.OPEN) {
597
- this.sendCommand(agentId, MessageTypes.UNSUBSCRIBE, { agentId });
598
- }
599
-
600
- this._cleanupConnection(agentId);
601
-
602
- this.logger.info?.(`[VisualEditorBridge] Disconnected from editor for agent: ${agentId}`);
603
- this.emit('editor-disconnected', { agentId });
604
-
605
- return true;
606
- }
607
-
608
- /**
609
- * Send command to visual editor
610
- * @param {string} agentId - Agent identifier
611
- * @param {string} type - Message type
612
- * @param {Object} data - Message data
613
- * @returns {boolean} Success
614
- */
615
- sendCommand(agentId, type, data = {}) {
616
- const instance = this.instances.get(agentId);
617
- if (!instance?.wsConnection || instance.wsConnection.readyState !== WebSocket.OPEN) {
618
- this.logger.warn?.(`[VisualEditorBridge] Cannot send command - not connected: ${agentId}`);
619
- return false;
620
- }
621
-
622
- try {
623
- const message = JSON.stringify({
624
- type,
625
- agentId,
626
- timestamp: Date.now(),
627
- ...data
628
- });
629
-
630
- instance.wsConnection.send(message);
631
- instance.lastActivity = Date.now();
632
- this._resetIdleTimer(agentId);
633
-
634
- this.logger.debug?.(`[VisualEditorBridge] Sent command: ${type} for agent: ${agentId}`);
635
- return true;
636
-
637
- } catch (error) {
638
- this.logger.error?.(`[VisualEditorBridge] Failed to send command: ${error.message}`);
639
- return false;
640
- }
641
- }
642
-
643
- /**
644
- * Highlight element in visual editor preview
645
- * @param {string} agentId - Agent identifier
646
- * @param {string} selector - CSS selector to highlight
647
- * @param {number} durationMs - Highlight duration (default: 2000ms)
648
- * @returns {boolean} Success
649
- */
650
- highlightElement(agentId, selector, durationMs = 2000) {
651
- return this.sendCommand(agentId, MessageTypes.HIGHLIGHT, {
652
- selector,
653
- duration: durationMs
654
- });
655
- }
656
-
657
- /**
658
- * Scroll to element in visual editor preview
659
- * @param {string} agentId - Agent identifier
660
- * @param {string} selector - CSS selector to scroll to
661
- * @returns {boolean} Success
662
- */
663
- scrollToElement(agentId, selector) {
664
- return this.sendCommand(agentId, MessageTypes.SCROLL_TO, { selector });
665
- }
666
-
667
- /**
668
- * Reload the visual editor preview
669
- * @param {string} agentId - Agent identifier
670
- * @returns {boolean} Success
671
- */
672
- reloadPreview(agentId) {
673
- return this.sendCommand(agentId, MessageTypes.RELOAD, {});
674
- }
675
-
676
- /**
677
- * Set visual editor mode
678
- * @param {string} agentId - Agent identifier
679
- * @param {string} mode - Mode ('edit' or 'preview')
680
- * @returns {boolean} Success
681
- */
682
- setEditorMode(agentId, mode) {
683
- if (!['edit', 'preview'].includes(mode)) {
684
- throw new Error(`Invalid mode: ${mode}. Must be 'edit' or 'preview'`);
685
- }
686
- return this.sendCommand(agentId, MessageTypes.SET_MODE, { mode });
687
- }
688
-
689
- /**
690
- * Check if instance is connected to editor
691
- * @param {string} agentId - Agent identifier
692
- * @returns {boolean}
693
- */
694
- isConnected(agentId) {
695
- const instance = this.instances.get(agentId);
696
- return instance?.wsConnection?.readyState === WebSocket.OPEN;
697
- }
698
-
699
- // === Private methods ===
700
-
701
- /**
702
- * Reset idle timer for instance
703
- * @private
704
- */
705
- _resetIdleTimer(agentId) {
706
- const instance = this.instances.get(agentId);
707
- if (!instance) return;
708
-
709
- if (instance.idleTimer) {
710
- clearTimeout(instance.idleTimer);
711
- }
712
-
713
- instance.idleTimer = setTimeout(() => {
714
- this.logger.info?.(`[VisualEditorBridge] Idle timeout for agent: ${agentId}`);
715
- this.stopInstance(agentId);
716
- }, this.idleTimeoutMs);
717
- }
718
-
719
- /**
720
- * Evict the oldest idle instance to make room
721
- * @private
722
- * @returns {boolean} Whether an instance was evicted
723
- */
724
- _evictOldestIdle() {
725
- let oldest = null;
726
- let oldestTime = Infinity;
727
-
728
- // First pass: prefer instances without active subscribers
729
- for (const [agentId, instance] of this.instances) {
730
- const hasSubscribers = instance.uiSubscribers.size > 0;
731
-
732
- if (!hasSubscribers && instance.lastActivity < oldestTime) {
733
- oldest = agentId;
734
- oldestTime = instance.lastActivity;
735
- }
736
- }
737
-
738
- // Second pass: if no instance without subscribers, evict oldest overall
739
- if (!oldest) {
740
- oldestTime = Infinity;
741
- for (const [agentId, instance] of this.instances) {
742
- if (instance.lastActivity < oldestTime) {
743
- oldest = agentId;
744
- oldestTime = instance.lastActivity;
745
- }
746
- }
747
- }
748
-
749
- if (oldest) {
750
- this.logger.info?.(`[VisualEditorBridge] Evicting idle instance: ${oldest}`);
751
- this.stopInstance(oldest);
752
- return true;
753
- }
754
-
755
- return false;
756
- }
757
-
758
- /**
759
- * Cleanup idle instances (called periodically)
760
- * @private
761
- */
762
- _cleanupIdle() {
763
- const now = Date.now();
764
- const toCleanup = [];
765
-
766
- for (const [agentId, instance] of this.instances) {
767
- if (now - instance.lastActivity > this.idleTimeoutMs) {
768
- toCleanup.push(agentId);
769
- }
770
- }
771
-
772
- for (const agentId of toCleanup) {
773
- this.logger.info?.(`[VisualEditorBridge] Cleanup idle instance: ${agentId}`);
774
- this.stopInstance(agentId);
775
- }
776
- }
777
-
778
- /**
779
- * Sanitize instance for external exposure (remove internals)
780
- * @private
781
- */
782
- _sanitizeInstance(instance) {
783
- return {
784
- agentId: instance.agentId,
785
- status: instance.status,
786
- projectRoot: instance.projectRoot,
787
- appUrl: instance.appUrl,
788
- editorUrl: instance.editorUrl,
789
- hasVisualContext: !!instance.visualContext,
790
- subscriberCount: instance.uiSubscribers.size,
791
- createdAt: instance.createdAt,
792
- lastActivity: instance.lastActivity,
793
- isConnected: instance.wsConnection?.readyState === WebSocket.OPEN
794
- };
795
- }
796
-
797
- /**
798
- * Handle incoming message from visual editor
799
- * @private
800
- */
801
- _handleEditorMessage(agentId, message) {
802
- const instance = this.instances.get(agentId);
803
- if (!instance) return;
804
-
805
- instance.lastActivity = Date.now();
806
- this._resetIdleTimer(agentId);
807
-
808
- const { type } = message;
809
-
810
- switch (type) {
811
- case MessageTypes.ELEMENT_SELECTED:
812
- // User selected an element in the visual editor
813
- this.logger.info?.(`[VisualEditorBridge] Element selected for agent: ${agentId}`, {
814
- selector: message.selector,
815
- sourceFile: message.sourceHint?.file
816
- });
817
-
818
- // Store visual context
819
- this.setVisualContext(agentId, {
820
- selector: message.selector,
821
- tagName: message.tagName,
822
- text: message.text,
823
- attributes: message.attributes,
824
- boundingRect: message.boundingRect,
825
- sourceHint: message.sourceHint
826
- });
827
-
828
- // Emit event for UI subscribers
829
- this.emit('element-selected', {
830
- agentId,
831
- element: message
832
- });
833
- break;
834
-
835
- case MessageTypes.FILE_CHANGED:
836
- // File changed in the project
837
- this.logger.info?.(`[VisualEditorBridge] File changed for agent: ${agentId}`, {
838
- file: message.file,
839
- type: message.changeType
840
- });
841
-
842
- this.emit('file-changed', {
843
- agentId,
844
- file: message.file,
845
- changeType: message.changeType
846
- });
847
- break;
848
-
849
- case MessageTypes.EDITOR_READY:
850
- // Editor is ready and connected
851
- this.logger.info?.(`[VisualEditorBridge] Editor ready for agent: ${agentId}`);
852
- this.emit('editor-ready', { agentId });
853
- break;
854
-
855
- case MessageTypes.PONG:
856
- // Heartbeat response
857
- instance.lastPong = Date.now();
858
- break;
859
-
860
- case MessageTypes.ERROR:
861
- // Error from editor
862
- this.logger.error?.(`[VisualEditorBridge] Editor error for agent: ${agentId}`, {
863
- error: message.error
864
- });
865
-
866
- instance.error = message.error;
867
- this.emit('editor-error', {
868
- agentId,
869
- error: message.error
870
- });
871
- break;
872
-
873
- default:
874
- this.logger.debug?.(`[VisualEditorBridge] Unknown message type: ${type}`);
875
- }
876
- }
877
-
878
- /**
879
- * Cleanup WebSocket connection resources
880
- * @private
881
- */
882
- _cleanupConnection(agentId) {
883
- const instance = this.instances.get(agentId);
884
- if (!instance) return;
885
-
886
- // Stop ping interval
887
- if (instance.pingInterval) {
888
- clearInterval(instance.pingInterval);
889
- instance.pingInterval = null;
890
- }
891
-
892
- // Cancel reconnect timer
893
- if (instance.reconnectTimer) {
894
- clearTimeout(instance.reconnectTimer);
895
- instance.reconnectTimer = null;
896
- }
897
-
898
- // Close WebSocket
899
- if (instance.wsConnection) {
900
- try {
901
- instance.wsConnection.close();
902
- } catch (err) {
903
- // Ignore
904
- }
905
- instance.wsConnection = null;
906
- }
907
-
908
- instance.isConnecting = false;
909
- instance.lastPong = null;
910
- }
911
-
912
- /**
913
- * Schedule reconnection with exponential backoff
914
- * @private
915
- */
916
- _scheduleReconnect(agentId) {
917
- const instance = this.instances.get(agentId);
918
- if (!instance) return;
919
-
920
- if (instance.reconnectAttempts >= this.maxReconnectAttempts) {
921
- this.logger.error?.(`[VisualEditorBridge] Max reconnect attempts reached for agent: ${agentId}`);
922
- instance.status = InstanceStatus.ERROR;
923
- instance.error = 'Max reconnection attempts reached';
924
- this.emit('instance-status-changed', {
925
- agentId,
926
- status: InstanceStatus.ERROR,
927
- error: instance.error
928
- });
929
- return;
930
- }
931
-
932
- instance.reconnectAttempts++;
933
-
934
- // Exponential backoff: base * 2^attempts (capped at 30s)
935
- const delay = Math.min(
936
- this.reconnectIntervalMs * Math.pow(2, instance.reconnectAttempts - 1),
937
- 30000
938
- );
939
-
940
- this.logger.info?.(`[VisualEditorBridge] Scheduling reconnect for agent: ${agentId}`, {
941
- attempt: instance.reconnectAttempts,
942
- delayMs: delay
943
- });
944
-
945
- instance.reconnectTimer = setTimeout(() => {
946
- if (this.instances.has(agentId)) {
947
- this.connectToEditor(agentId).catch(err => {
948
- this.logger.error?.(`[VisualEditorBridge] Reconnect failed: ${err.message}`);
949
- });
950
- }
951
- }, delay);
952
- }
953
-
954
- /**
955
- * Start ping/pong heartbeat interval
956
- * @private
957
- */
958
- _startPingInterval(agentId) {
959
- const instance = this.instances.get(agentId);
960
- if (!instance) return;
961
-
962
- // Clear existing interval
963
- if (instance.pingInterval) {
964
- clearInterval(instance.pingInterval);
965
- }
966
-
967
- // Ping every 30 seconds
968
- instance.pingInterval = setInterval(() => {
969
- if (instance.wsConnection?.readyState === WebSocket.OPEN) {
970
- // Check if last pong was too long ago (60s timeout)
971
- if (instance.lastPong && Date.now() - instance.lastPong > 60000) {
972
- this.logger.warn?.(`[VisualEditorBridge] Ping timeout for agent: ${agentId}`);
973
- this._cleanupConnection(agentId);
974
- this._scheduleReconnect(agentId);
975
- return;
976
- }
977
-
978
- this.sendCommand(agentId, MessageTypes.PING, {});
979
- }
980
- }, 30000);
981
- }
982
-
983
- /**
984
- * Graceful shutdown - stop all instances
985
- */
986
- async shutdown() {
987
- this.logger.info?.('[VisualEditorBridge] Shutting down...');
988
-
989
- // Stop cleanup interval
990
- if (this.cleanupInterval) {
991
- clearInterval(this.cleanupInterval);
992
- this.cleanupInterval = null;
993
- }
994
-
995
- // Stop all instances
996
- const stopPromises = [];
997
- for (const agentId of this.instances.keys()) {
998
- stopPromises.push(this.stopInstance(agentId));
999
- }
1000
-
1001
- await Promise.all(stopPromises);
1002
-
1003
- this.logger.info?.('[VisualEditorBridge] Shutdown complete');
1004
- this.emit('shutdown');
1005
- }
1006
- }
1007
-
1008
- // Export singleton factory
1009
- let bridgeInstance = null;
1010
-
1011
- /**
1012
- * Get or create the bridge singleton
1013
- * @param {Object} config - Configuration (only used on first call)
1014
- * @returns {VisualEditorBridge}
1015
- */
1016
- export function getVisualEditorBridge(config = {}) {
1017
- if (!bridgeInstance) {
1018
- bridgeInstance = new VisualEditorBridge(config);
1019
- }
1020
- return bridgeInstance;
1021
- }
1022
-
1023
- /**
1024
- * Reset the singleton (for testing)
1025
- */
1026
- export function resetVisualEditorBridge() {
1027
- if (bridgeInstance) {
1028
- bridgeInstance.shutdown();
1029
- bridgeInstance = null;
1030
- }
1031
- }
1032
-
1033
- export default VisualEditorBridge;
1
+ /**
2
+ * Visual Editor Bridge Service
3
+ *
4
+ * Manages visual editor instances per agent, enabling users to interact
5
+ * with their web applications visually and give the AI agent pointers
6
+ * to code parts of interest.
7
+ *
8
+ * Key responsibilities:
9
+ * - Instance registry (one editor per agent)
10
+ * - Visual context storage (element selections)
11
+ * - Lifecycle management (create/stop/cleanup)
12
+ * - Multi-instance coordination
13
+ * - WebSocket connection to visual editor (Phase 3)
14
+ *
15
+ * @see VISUAL-EDITOR-INTEGRATION-PLAN.md for full architecture
16
+ */
17
+
18
+ import { EventEmitter } from 'events';
19
+ import WebSocket from 'ws';
20
+ import { getVisualEditorPort } from './visualEditorServer.js';
21
+
22
+ // Configuration defaults - port can be configured via config file, env var, or fallback
23
+ const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
24
+ const DEFAULT_MAX_INSTANCES = 3;
25
+ const DEFAULT_RECONNECT_INTERVAL_MS = 3000;
26
+ const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
27
+
28
+ /**
29
+ * Get the WebSocket URL for the visual editor (computed at runtime)
30
+ * Uses getVisualEditorPort which respects config file and env var settings
31
+ */
32
+ function getVisualEditorWsUrl() {
33
+ const port = getVisualEditorPort();
34
+ return `ws://localhost:${port}/ws`;
35
+ }
36
+
37
+ /**
38
+ * WebSocket message types from visual editor
39
+ */
40
+ export const MessageTypes = {
41
+ // Incoming from editor
42
+ ELEMENT_SELECTED: 'element-selected',
43
+ FILE_CHANGED: 'file-changed',
44
+ EDITOR_READY: 'editor-ready',
45
+ ERROR: 'error',
46
+ PONG: 'pong',
47
+
48
+ // Outgoing to editor
49
+ HIGHLIGHT: 'highlight',
50
+ SCROLL_TO: 'scroll-to',
51
+ RELOAD: 'reload',
52
+ SET_MODE: 'set-mode',
53
+ PING: 'ping',
54
+ SUBSCRIBE: 'subscribe',
55
+ UNSUBSCRIBE: 'unsubscribe'
56
+ };
57
+
58
+ /**
59
+ * Instance status enum
60
+ */
61
+ export const InstanceStatus = {
62
+ INITIALIZED: 'initialized',
63
+ CONNECTING: 'connecting',
64
+ READY: 'ready',
65
+ ERROR: 'error',
66
+ STOPPED: 'stopped'
67
+ };
68
+
69
+ /**
70
+ * Visual Editor Bridge Service
71
+ * Manages visual editor instances and coordinates communication
72
+ */
73
+ class VisualEditorBridge extends EventEmitter {
74
+ /**
75
+ * @param {Object} config - Configuration options
76
+ * @param {number} config.maxInstances - Maximum concurrent editors (default: 3)
77
+ * @param {number} config.idleTimeoutMs - Idle timeout in ms (default: 10 min)
78
+ * @param {string} config.visualEditorUrl - WebSocket URL for visual editor
79
+ * @param {number} config.reconnectIntervalMs - Reconnection interval
80
+ * @param {number} config.maxReconnectAttempts - Max reconnection attempts
81
+ * @param {Object} config.logger - Logger instance
82
+ */
83
+ constructor(config = {}) {
84
+ super();
85
+
86
+ // Instance registry: agentId → InstanceRecord
87
+ this.instances = new Map();
88
+
89
+ // Configuration
90
+ this.maxInstances = config.maxInstances || DEFAULT_MAX_INSTANCES;
91
+ this.idleTimeoutMs = config.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
92
+ this.visualEditorUrl = config.visualEditorUrl || getVisualEditorWsUrl();
93
+ this.reconnectIntervalMs = config.reconnectIntervalMs || DEFAULT_RECONNECT_INTERVAL_MS;
94
+ this.maxReconnectAttempts = config.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS;
95
+ this.logger = config.logger || console;
96
+ this.enabled = config.enabled !== false;
97
+
98
+ // Cleanup interval for orphaned instances
99
+ this.cleanupInterval = null;
100
+ if (this.enabled) {
101
+ this.cleanupInterval = setInterval(() => this._cleanupIdle(), 60000);
102
+ }
103
+
104
+ this.logger.info?.('[VisualEditorBridge] Initialized', {
105
+ maxInstances: this.maxInstances,
106
+ idleTimeoutMs: this.idleTimeoutMs,
107
+ visualEditorUrl: this.visualEditorUrl,
108
+ enabled: this.enabled
109
+ }) || this.logger.log('[VisualEditorBridge] Initialized');
110
+ }
111
+
112
+ /**
113
+ * Check if bridge is enabled
114
+ * @returns {boolean}
115
+ */
116
+ isEnabled() {
117
+ return this.enabled;
118
+ }
119
+
120
+ /**
121
+ * Get or create instance for agent
122
+ * @param {string} agentId - Agent identifier
123
+ * @param {Object} options - Instance options
124
+ * @param {string} options.projectRoot - Project root directory
125
+ * @param {string} options.appUrl - User's app URL to proxy
126
+ * @returns {Object} Instance record
127
+ * @throws {Error} If max instances reached
128
+ */
129
+ async getInstance(agentId, options = {}) {
130
+ if (!agentId) {
131
+ throw new Error('agentId is required');
132
+ }
133
+
134
+ // Return existing instance
135
+ if (this.instances.has(agentId)) {
136
+ const instance = this.instances.get(agentId);
137
+ instance.lastActivity = Date.now();
138
+ this._resetIdleTimer(agentId);
139
+
140
+ // Update options if provided
141
+ if (options.projectRoot) instance.projectRoot = options.projectRoot;
142
+ if (options.appUrl) instance.appUrl = options.appUrl;
143
+
144
+ return instance;
145
+ }
146
+
147
+ // Check instance limit
148
+ if (this.instances.size >= this.maxInstances) {
149
+ // Try to evict oldest idle instance
150
+ const evicted = this._evictOldestIdle();
151
+ if (!evicted) {
152
+ throw new Error(
153
+ `Maximum visual editor instances (${this.maxInstances}) reached. ` +
154
+ `Stop an existing editor first.`
155
+ );
156
+ }
157
+ }
158
+
159
+ // Create new instance record
160
+ const instance = {
161
+ agentId,
162
+ projectRoot: options.projectRoot || null,
163
+ appUrl: options.appUrl || null,
164
+ status: InstanceStatus.INITIALIZED,
165
+ wsConnection: null,
166
+ editorUrl: null,
167
+ lastActivity: Date.now(),
168
+ createdAt: Date.now(),
169
+ uiSubscribers: new Set(),
170
+ visualContext: null,
171
+ idleTimer: null,
172
+ error: null,
173
+ // WebSocket connection state (Phase 3)
174
+ reconnectAttempts: 0,
175
+ reconnectTimer: null,
176
+ pingInterval: null,
177
+ lastPong: null,
178
+ isConnecting: false
179
+ };
180
+
181
+ this.instances.set(agentId, instance);
182
+ this._resetIdleTimer(agentId);
183
+
184
+ this.logger.info?.(`[VisualEditorBridge] Created instance for agent: ${agentId}`, {
185
+ projectRoot: instance.projectRoot,
186
+ appUrl: instance.appUrl
187
+ }) || this.logger.log(`[VisualEditorBridge] Created instance: ${agentId}`);
188
+
189
+ this.emit('instance-created', { agentId, instance: this._sanitizeInstance(instance) });
190
+
191
+ return instance;
192
+ }
193
+
194
+ /**
195
+ * Check if instance exists for agent
196
+ * @param {string} agentId - Agent identifier
197
+ * @returns {boolean}
198
+ */
199
+ hasInstance(agentId) {
200
+ return this.instances.has(agentId);
201
+ }
202
+
203
+ /**
204
+ * Store visual context (element selection) for agent
205
+ * @param {string} agentId - Agent identifier
206
+ * @param {Object} elementReference - Element reference from visual editor
207
+ * @returns {boolean} Success
208
+ */
209
+ setVisualContext(agentId, elementReference) {
210
+ const instance = this.instances.get(agentId);
211
+ if (!instance) {
212
+ this.logger.warn?.(`[VisualEditorBridge] No instance for agent: ${agentId}`) ||
213
+ this.logger.log(`[VisualEditorBridge] No instance: ${agentId}`);
214
+ return false;
215
+ }
216
+
217
+ instance.visualContext = {
218
+ ...elementReference,
219
+ receivedAt: new Date().toISOString()
220
+ };
221
+ instance.lastActivity = Date.now();
222
+ this._resetIdleTimer(agentId);
223
+
224
+ this.logger.info?.(`[VisualEditorBridge] Visual context set for agent: ${agentId}`, {
225
+ selector: elementReference.selector,
226
+ sourceFile: elementReference.sourceHint?.file
227
+ }) || this.logger.log(`[VisualEditorBridge] Context set: ${agentId}`);
228
+
229
+ this.emit('visual-context-updated', {
230
+ agentId,
231
+ context: instance.visualContext
232
+ });
233
+
234
+ return true;
235
+ }
236
+
237
+ /**
238
+ * Get visual context for agent
239
+ * @param {string} agentId - Agent identifier
240
+ * @returns {Object|null} Visual context or null
241
+ */
242
+ getVisualContext(agentId) {
243
+ const instance = this.instances.get(agentId);
244
+ return instance?.visualContext || null;
245
+ }
246
+
247
+ /**
248
+ * Clear visual context for agent
249
+ * @param {string} agentId - Agent identifier
250
+ * @returns {boolean} Success
251
+ */
252
+ clearVisualContext(agentId) {
253
+ const instance = this.instances.get(agentId);
254
+ if (instance && instance.visualContext) {
255
+ instance.visualContext = null;
256
+ this.emit('visual-context-cleared', { agentId });
257
+ return true;
258
+ }
259
+ return false;
260
+ }
261
+
262
+ /**
263
+ * Get instance status
264
+ * @param {string} agentId - Agent identifier
265
+ * @returns {Object} Status object
266
+ */
267
+ getStatus(agentId) {
268
+ const instance = this.instances.get(agentId);
269
+ if (!instance) {
270
+ return {
271
+ exists: false,
272
+ agentId
273
+ };
274
+ }
275
+
276
+ return {
277
+ exists: true,
278
+ agentId: instance.agentId,
279
+ status: instance.status,
280
+ projectRoot: instance.projectRoot,
281
+ appUrl: instance.appUrl,
282
+ editorUrl: instance.editorUrl,
283
+ hasVisualContext: !!instance.visualContext,
284
+ visualContext: instance.visualContext,
285
+ subscriberCount: instance.uiSubscribers.size,
286
+ createdAt: instance.createdAt,
287
+ lastActivity: instance.lastActivity,
288
+ idleMs: Date.now() - instance.lastActivity,
289
+ error: instance.error
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Update instance status
295
+ * @param {string} agentId - Agent identifier
296
+ * @param {string} status - New status
297
+ * @param {Object} extra - Additional fields to update
298
+ * @returns {boolean} Success
299
+ */
300
+ updateStatus(agentId, status, extra = {}) {
301
+ const instance = this.instances.get(agentId);
302
+ if (!instance) {
303
+ return false;
304
+ }
305
+
306
+ instance.status = status;
307
+ instance.lastActivity = Date.now();
308
+
309
+ if (extra.editorUrl) instance.editorUrl = extra.editorUrl;
310
+ if (extra.wsConnection) instance.wsConnection = extra.wsConnection;
311
+ if (extra.error) instance.error = extra.error;
312
+
313
+ this.emit('instance-status-changed', {
314
+ agentId,
315
+ status,
316
+ ...extra
317
+ });
318
+
319
+ return true;
320
+ }
321
+
322
+ /**
323
+ * Add UI subscriber to instance
324
+ * @param {string} agentId - Agent identifier
325
+ * @param {string} connectionId - UI connection identifier
326
+ * @returns {boolean} Success
327
+ */
328
+ addSubscriber(agentId, connectionId) {
329
+ const instance = this.instances.get(agentId);
330
+ if (!instance) {
331
+ return false;
332
+ }
333
+
334
+ instance.uiSubscribers.add(connectionId);
335
+ instance.lastActivity = Date.now();
336
+ return true;
337
+ }
338
+
339
+ /**
340
+ * Remove UI subscriber from instance
341
+ * @param {string} agentId - Agent identifier
342
+ * @param {string} connectionId - UI connection identifier
343
+ * @returns {boolean} Success
344
+ */
345
+ removeSubscriber(agentId, connectionId) {
346
+ const instance = this.instances.get(agentId);
347
+ if (!instance) {
348
+ return false;
349
+ }
350
+
351
+ return instance.uiSubscribers.delete(connectionId);
352
+ }
353
+
354
+ /**
355
+ * Stop and remove instance for agent
356
+ * @param {string} agentId - Agent identifier
357
+ * @returns {boolean} Success
358
+ */
359
+ async stopInstance(agentId) {
360
+ const instance = this.instances.get(agentId);
361
+ if (!instance) {
362
+ return false;
363
+ }
364
+
365
+ // Mark as stopped to prevent reconnection attempts
366
+ instance.status = InstanceStatus.STOPPED;
367
+
368
+ // Clear idle timer
369
+ if (instance.idleTimer) {
370
+ clearTimeout(instance.idleTimer);
371
+ instance.idleTimer = null;
372
+ }
373
+
374
+ // Cleanup WebSocket connection (including ping interval and reconnect timer)
375
+ this._cleanupConnection(agentId);
376
+
377
+ // Remove from registry
378
+ this.instances.delete(agentId);
379
+
380
+ this.logger.info?.(`[VisualEditorBridge] Stopped instance for agent: ${agentId}`) ||
381
+ this.logger.log(`[VisualEditorBridge] Stopped: ${agentId}`);
382
+
383
+ this.emit('instance-stopped', { agentId });
384
+
385
+ return true;
386
+ }
387
+
388
+ /**
389
+ * Handle agent deletion - cleanup instance
390
+ * @param {string} agentId - Agent identifier
391
+ * @returns {boolean} Success
392
+ */
393
+ onAgentDeleted(agentId) {
394
+ this.logger.info?.(`[VisualEditorBridge] Agent deleted, cleaning up: ${agentId}`);
395
+ return this.stopInstance(agentId);
396
+ }
397
+
398
+ /**
399
+ * Handle agent unload - cleanup instance
400
+ * @param {string} agentId - Agent identifier
401
+ * @returns {boolean} Success
402
+ */
403
+ onAgentUnloaded(agentId) {
404
+ this.logger.info?.(`[VisualEditorBridge] Agent unloaded, cleaning up: ${agentId}`);
405
+ return this.stopInstance(agentId);
406
+ }
407
+
408
+ /**
409
+ * Check if project is used by another agent
410
+ * @param {string} agentId - Current agent identifier
411
+ * @param {string} projectRoot - Project root to check
412
+ * @returns {Object} Collision info
413
+ */
414
+ checkProjectCollision(agentId, projectRoot) {
415
+ if (!projectRoot) {
416
+ return { collision: false };
417
+ }
418
+
419
+ for (const [otherId, instance] of this.instances) {
420
+ if (otherId !== agentId && instance.projectRoot === projectRoot) {
421
+ return {
422
+ collision: true,
423
+ otherAgentId: otherId,
424
+ message: `Project "${projectRoot}" is already being edited by agent "${otherId}"`
425
+ };
426
+ }
427
+ }
428
+ return { collision: false };
429
+ }
430
+
431
+ /**
432
+ * List all instances
433
+ * @returns {Array} Array of instance info objects
434
+ */
435
+ listInstances() {
436
+ return Array.from(this.instances.entries()).map(([agentId, instance]) => ({
437
+ agentId,
438
+ status: instance.status,
439
+ projectRoot: instance.projectRoot,
440
+ appUrl: instance.appUrl,
441
+ hasContext: !!instance.visualContext,
442
+ subscriberCount: instance.uiSubscribers.size,
443
+ createdAt: instance.createdAt,
444
+ lastActivity: instance.lastActivity,
445
+ idleMs: Date.now() - instance.lastActivity
446
+ }));
447
+ }
448
+
449
+ /**
450
+ * Get count of active instances
451
+ * @returns {number}
452
+ */
453
+ getInstanceCount() {
454
+ return this.instances.size;
455
+ }
456
+
457
+ /**
458
+ * Touch instance to reset idle timer
459
+ * @param {string} agentId - Agent identifier
460
+ */
461
+ touchInstance(agentId) {
462
+ const instance = this.instances.get(agentId);
463
+ if (instance) {
464
+ instance.lastActivity = Date.now();
465
+ this._resetIdleTimer(agentId);
466
+ }
467
+ }
468
+
469
+ // === WebSocket Methods (Phase 3) ===
470
+
471
+ /**
472
+ * Connect to visual editor WebSocket
473
+ * @param {string} agentId - Agent identifier
474
+ * @param {Object} options - Connection options
475
+ * @param {string} options.editorUrl - Override editor URL
476
+ * @returns {Promise<boolean>} Connection success
477
+ */
478
+ async connectToEditor(agentId, options = {}) {
479
+ const instance = this.instances.get(agentId);
480
+ if (!instance) {
481
+ throw new Error(`No instance for agent: ${agentId}`);
482
+ }
483
+
484
+ // Already connected or connecting
485
+ if (instance.wsConnection?.readyState === WebSocket.OPEN) {
486
+ return true;
487
+ }
488
+
489
+ if (instance.isConnecting) {
490
+ return false;
491
+ }
492
+
493
+ instance.isConnecting = true;
494
+ instance.status = InstanceStatus.CONNECTING;
495
+ this.emit('instance-status-changed', { agentId, status: InstanceStatus.CONNECTING });
496
+
497
+ const editorUrl = options.editorUrl || this.visualEditorUrl;
498
+
499
+ return new Promise((resolve) => {
500
+ try {
501
+ this.logger.info?.(`[VisualEditorBridge] Connecting to editor for agent: ${agentId}`, {
502
+ url: editorUrl
503
+ });
504
+
505
+ const ws = new WebSocket(editorUrl);
506
+
507
+ ws.on('open', () => {
508
+ instance.wsConnection = ws;
509
+ instance.status = InstanceStatus.READY;
510
+ instance.editorUrl = editorUrl;
511
+ instance.isConnecting = false;
512
+ instance.reconnectAttempts = 0;
513
+ instance.error = null;
514
+ instance.lastActivity = Date.now();
515
+
516
+ this.logger.info?.(`[VisualEditorBridge] Connected to editor for agent: ${agentId}`);
517
+
518
+ // Start heartbeat
519
+ this._startPingInterval(agentId);
520
+
521
+ // Subscribe to editor events for this agent
522
+ this.sendCommand(agentId, MessageTypes.SUBSCRIBE, {
523
+ agentId,
524
+ projectRoot: instance.projectRoot,
525
+ appUrl: instance.appUrl
526
+ });
527
+
528
+ this.emit('editor-connected', { agentId, editorUrl });
529
+ this.emit('instance-status-changed', { agentId, status: InstanceStatus.READY });
530
+
531
+ resolve(true);
532
+ });
533
+
534
+ ws.on('message', (data) => {
535
+ try {
536
+ const message = JSON.parse(data.toString());
537
+ this._handleEditorMessage(agentId, message);
538
+ } catch (err) {
539
+ this.logger.warn?.(`[VisualEditorBridge] Invalid message from editor: ${err.message}`);
540
+ }
541
+ });
542
+
543
+ ws.on('close', (code, reason) => {
544
+ this.logger.info?.(`[VisualEditorBridge] Editor connection closed for agent: ${agentId}`, {
545
+ code,
546
+ reason: reason?.toString()
547
+ });
548
+
549
+ this._cleanupConnection(agentId);
550
+
551
+ // Schedule reconnect if instance still exists and wasn't manually stopped
552
+ if (this.instances.has(agentId) && instance.status !== InstanceStatus.STOPPED) {
553
+ this._scheduleReconnect(agentId);
554
+ }
555
+ });
556
+
557
+ ws.on('error', (error) => {
558
+ this.logger.error?.(`[VisualEditorBridge] Editor connection error for agent: ${agentId}`, {
559
+ error: error.message
560
+ });
561
+
562
+ instance.error = error.message;
563
+ instance.isConnecting = false;
564
+
565
+ if (instance.status === InstanceStatus.CONNECTING) {
566
+ instance.status = InstanceStatus.ERROR;
567
+ this.emit('instance-status-changed', { agentId, status: InstanceStatus.ERROR, error: error.message });
568
+ }
569
+
570
+ this.emit('editor-error', { agentId, error: error.message });
571
+ resolve(false);
572
+ });
573
+
574
+ } catch (error) {
575
+ instance.isConnecting = false;
576
+ instance.status = InstanceStatus.ERROR;
577
+ instance.error = error.message;
578
+ this.logger.error?.(`[VisualEditorBridge] Failed to create WebSocket: ${error.message}`);
579
+ resolve(false);
580
+ }
581
+ });
582
+ }
583
+
584
+ /**
585
+ * Disconnect from visual editor
586
+ * @param {string} agentId - Agent identifier
587
+ * @returns {boolean} Success
588
+ */
589
+ disconnectFromEditor(agentId) {
590
+ const instance = this.instances.get(agentId);
591
+ if (!instance) {
592
+ return false;
593
+ }
594
+
595
+ // Send unsubscribe before closing
596
+ if (instance.wsConnection?.readyState === WebSocket.OPEN) {
597
+ this.sendCommand(agentId, MessageTypes.UNSUBSCRIBE, { agentId });
598
+ }
599
+
600
+ this._cleanupConnection(agentId);
601
+
602
+ this.logger.info?.(`[VisualEditorBridge] Disconnected from editor for agent: ${agentId}`);
603
+ this.emit('editor-disconnected', { agentId });
604
+
605
+ return true;
606
+ }
607
+
608
+ /**
609
+ * Send command to visual editor
610
+ * @param {string} agentId - Agent identifier
611
+ * @param {string} type - Message type
612
+ * @param {Object} data - Message data
613
+ * @returns {boolean} Success
614
+ */
615
+ sendCommand(agentId, type, data = {}) {
616
+ const instance = this.instances.get(agentId);
617
+ if (!instance?.wsConnection || instance.wsConnection.readyState !== WebSocket.OPEN) {
618
+ this.logger.warn?.(`[VisualEditorBridge] Cannot send command - not connected: ${agentId}`);
619
+ return false;
620
+ }
621
+
622
+ try {
623
+ const message = JSON.stringify({
624
+ type,
625
+ agentId,
626
+ timestamp: Date.now(),
627
+ ...data
628
+ });
629
+
630
+ instance.wsConnection.send(message);
631
+ instance.lastActivity = Date.now();
632
+ this._resetIdleTimer(agentId);
633
+
634
+ this.logger.debug?.(`[VisualEditorBridge] Sent command: ${type} for agent: ${agentId}`);
635
+ return true;
636
+
637
+ } catch (error) {
638
+ this.logger.error?.(`[VisualEditorBridge] Failed to send command: ${error.message}`);
639
+ return false;
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Highlight element in visual editor preview
645
+ * @param {string} agentId - Agent identifier
646
+ * @param {string} selector - CSS selector to highlight
647
+ * @param {number} durationMs - Highlight duration (default: 2000ms)
648
+ * @returns {boolean} Success
649
+ */
650
+ highlightElement(agentId, selector, durationMs = 2000) {
651
+ return this.sendCommand(agentId, MessageTypes.HIGHLIGHT, {
652
+ selector,
653
+ duration: durationMs
654
+ });
655
+ }
656
+
657
+ /**
658
+ * Scroll to element in visual editor preview
659
+ * @param {string} agentId - Agent identifier
660
+ * @param {string} selector - CSS selector to scroll to
661
+ * @returns {boolean} Success
662
+ */
663
+ scrollToElement(agentId, selector) {
664
+ return this.sendCommand(agentId, MessageTypes.SCROLL_TO, { selector });
665
+ }
666
+
667
+ /**
668
+ * Reload the visual editor preview
669
+ * @param {string} agentId - Agent identifier
670
+ * @returns {boolean} Success
671
+ */
672
+ reloadPreview(agentId) {
673
+ return this.sendCommand(agentId, MessageTypes.RELOAD, {});
674
+ }
675
+
676
+ /**
677
+ * Set visual editor mode
678
+ * @param {string} agentId - Agent identifier
679
+ * @param {string} mode - Mode ('edit' or 'preview')
680
+ * @returns {boolean} Success
681
+ */
682
+ setEditorMode(agentId, mode) {
683
+ if (!['edit', 'preview'].includes(mode)) {
684
+ throw new Error(`Invalid mode: ${mode}. Must be 'edit' or 'preview'`);
685
+ }
686
+ return this.sendCommand(agentId, MessageTypes.SET_MODE, { mode });
687
+ }
688
+
689
+ /**
690
+ * Check if instance is connected to editor
691
+ * @param {string} agentId - Agent identifier
692
+ * @returns {boolean}
693
+ */
694
+ isConnected(agentId) {
695
+ const instance = this.instances.get(agentId);
696
+ return instance?.wsConnection?.readyState === WebSocket.OPEN;
697
+ }
698
+
699
+ // === Private methods ===
700
+
701
+ /**
702
+ * Reset idle timer for instance
703
+ * @private
704
+ */
705
+ _resetIdleTimer(agentId) {
706
+ const instance = this.instances.get(agentId);
707
+ if (!instance) return;
708
+
709
+ if (instance.idleTimer) {
710
+ clearTimeout(instance.idleTimer);
711
+ }
712
+
713
+ instance.idleTimer = setTimeout(() => {
714
+ this.logger.info?.(`[VisualEditorBridge] Idle timeout for agent: ${agentId}`);
715
+ this.stopInstance(agentId);
716
+ }, this.idleTimeoutMs);
717
+ }
718
+
719
+ /**
720
+ * Evict the oldest idle instance to make room
721
+ * @private
722
+ * @returns {boolean} Whether an instance was evicted
723
+ */
724
+ _evictOldestIdle() {
725
+ let oldest = null;
726
+ let oldestTime = Infinity;
727
+
728
+ // First pass: prefer instances without active subscribers
729
+ for (const [agentId, instance] of this.instances) {
730
+ const hasSubscribers = instance.uiSubscribers.size > 0;
731
+
732
+ if (!hasSubscribers && instance.lastActivity < oldestTime) {
733
+ oldest = agentId;
734
+ oldestTime = instance.lastActivity;
735
+ }
736
+ }
737
+
738
+ // Second pass: if no instance without subscribers, evict oldest overall
739
+ if (!oldest) {
740
+ oldestTime = Infinity;
741
+ for (const [agentId, instance] of this.instances) {
742
+ if (instance.lastActivity < oldestTime) {
743
+ oldest = agentId;
744
+ oldestTime = instance.lastActivity;
745
+ }
746
+ }
747
+ }
748
+
749
+ if (oldest) {
750
+ this.logger.info?.(`[VisualEditorBridge] Evicting idle instance: ${oldest}`);
751
+ this.stopInstance(oldest);
752
+ return true;
753
+ }
754
+
755
+ return false;
756
+ }
757
+
758
+ /**
759
+ * Cleanup idle instances (called periodically)
760
+ * @private
761
+ */
762
+ _cleanupIdle() {
763
+ const now = Date.now();
764
+ const toCleanup = [];
765
+
766
+ for (const [agentId, instance] of this.instances) {
767
+ if (now - instance.lastActivity > this.idleTimeoutMs) {
768
+ toCleanup.push(agentId);
769
+ }
770
+ }
771
+
772
+ for (const agentId of toCleanup) {
773
+ this.logger.info?.(`[VisualEditorBridge] Cleanup idle instance: ${agentId}`);
774
+ this.stopInstance(agentId);
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Sanitize instance for external exposure (remove internals)
780
+ * @private
781
+ */
782
+ _sanitizeInstance(instance) {
783
+ return {
784
+ agentId: instance.agentId,
785
+ status: instance.status,
786
+ projectRoot: instance.projectRoot,
787
+ appUrl: instance.appUrl,
788
+ editorUrl: instance.editorUrl,
789
+ hasVisualContext: !!instance.visualContext,
790
+ subscriberCount: instance.uiSubscribers.size,
791
+ createdAt: instance.createdAt,
792
+ lastActivity: instance.lastActivity,
793
+ isConnected: instance.wsConnection?.readyState === WebSocket.OPEN
794
+ };
795
+ }
796
+
797
+ /**
798
+ * Handle incoming message from visual editor
799
+ * @private
800
+ */
801
+ _handleEditorMessage(agentId, message) {
802
+ const instance = this.instances.get(agentId);
803
+ if (!instance) return;
804
+
805
+ instance.lastActivity = Date.now();
806
+ this._resetIdleTimer(agentId);
807
+
808
+ const { type } = message;
809
+
810
+ switch (type) {
811
+ case MessageTypes.ELEMENT_SELECTED:
812
+ // User selected an element in the visual editor
813
+ this.logger.info?.(`[VisualEditorBridge] Element selected for agent: ${agentId}`, {
814
+ selector: message.selector,
815
+ sourceFile: message.sourceHint?.file
816
+ });
817
+
818
+ // Store visual context
819
+ this.setVisualContext(agentId, {
820
+ selector: message.selector,
821
+ tagName: message.tagName,
822
+ text: message.text,
823
+ attributes: message.attributes,
824
+ boundingRect: message.boundingRect,
825
+ sourceHint: message.sourceHint
826
+ });
827
+
828
+ // Emit event for UI subscribers
829
+ this.emit('element-selected', {
830
+ agentId,
831
+ element: message
832
+ });
833
+ break;
834
+
835
+ case MessageTypes.FILE_CHANGED:
836
+ // File changed in the project
837
+ this.logger.info?.(`[VisualEditorBridge] File changed for agent: ${agentId}`, {
838
+ file: message.file,
839
+ type: message.changeType
840
+ });
841
+
842
+ this.emit('file-changed', {
843
+ agentId,
844
+ file: message.file,
845
+ changeType: message.changeType
846
+ });
847
+ break;
848
+
849
+ case MessageTypes.EDITOR_READY:
850
+ // Editor is ready and connected
851
+ this.logger.info?.(`[VisualEditorBridge] Editor ready for agent: ${agentId}`);
852
+ this.emit('editor-ready', { agentId });
853
+ break;
854
+
855
+ case MessageTypes.PONG:
856
+ // Heartbeat response
857
+ instance.lastPong = Date.now();
858
+ break;
859
+
860
+ case MessageTypes.ERROR:
861
+ // Error from editor
862
+ this.logger.error?.(`[VisualEditorBridge] Editor error for agent: ${agentId}`, {
863
+ error: message.error
864
+ });
865
+
866
+ instance.error = message.error;
867
+ this.emit('editor-error', {
868
+ agentId,
869
+ error: message.error
870
+ });
871
+ break;
872
+
873
+ default:
874
+ this.logger.debug?.(`[VisualEditorBridge] Unknown message type: ${type}`);
875
+ }
876
+ }
877
+
878
+ /**
879
+ * Cleanup WebSocket connection resources
880
+ * @private
881
+ */
882
+ _cleanupConnection(agentId) {
883
+ const instance = this.instances.get(agentId);
884
+ if (!instance) return;
885
+
886
+ // Stop ping interval
887
+ if (instance.pingInterval) {
888
+ clearInterval(instance.pingInterval);
889
+ instance.pingInterval = null;
890
+ }
891
+
892
+ // Cancel reconnect timer
893
+ if (instance.reconnectTimer) {
894
+ clearTimeout(instance.reconnectTimer);
895
+ instance.reconnectTimer = null;
896
+ }
897
+
898
+ // Close WebSocket
899
+ if (instance.wsConnection) {
900
+ try {
901
+ instance.wsConnection.close();
902
+ } catch {
903
+ // Ignore
904
+ }
905
+ instance.wsConnection = null;
906
+ }
907
+
908
+ instance.isConnecting = false;
909
+ instance.lastPong = null;
910
+ }
911
+
912
+ /**
913
+ * Schedule reconnection with exponential backoff
914
+ * @private
915
+ */
916
+ _scheduleReconnect(agentId) {
917
+ const instance = this.instances.get(agentId);
918
+ if (!instance) return;
919
+
920
+ if (instance.reconnectAttempts >= this.maxReconnectAttempts) {
921
+ this.logger.error?.(`[VisualEditorBridge] Max reconnect attempts reached for agent: ${agentId}`);
922
+ instance.status = InstanceStatus.ERROR;
923
+ instance.error = 'Max reconnection attempts reached';
924
+ this.emit('instance-status-changed', {
925
+ agentId,
926
+ status: InstanceStatus.ERROR,
927
+ error: instance.error
928
+ });
929
+ return;
930
+ }
931
+
932
+ instance.reconnectAttempts++;
933
+
934
+ // Exponential backoff: base * 2^attempts (capped at 30s)
935
+ const delay = Math.min(
936
+ this.reconnectIntervalMs * Math.pow(2, instance.reconnectAttempts - 1),
937
+ 30000
938
+ );
939
+
940
+ this.logger.info?.(`[VisualEditorBridge] Scheduling reconnect for agent: ${agentId}`, {
941
+ attempt: instance.reconnectAttempts,
942
+ delayMs: delay
943
+ });
944
+
945
+ instance.reconnectTimer = setTimeout(() => {
946
+ if (this.instances.has(agentId)) {
947
+ this.connectToEditor(agentId).catch(err => {
948
+ this.logger.error?.(`[VisualEditorBridge] Reconnect failed: ${err.message}`);
949
+ });
950
+ }
951
+ }, delay);
952
+ }
953
+
954
+ /**
955
+ * Start ping/pong heartbeat interval
956
+ * @private
957
+ */
958
+ _startPingInterval(agentId) {
959
+ const instance = this.instances.get(agentId);
960
+ if (!instance) return;
961
+
962
+ // Clear existing interval
963
+ if (instance.pingInterval) {
964
+ clearInterval(instance.pingInterval);
965
+ }
966
+
967
+ // Ping every 30 seconds
968
+ instance.pingInterval = setInterval(() => {
969
+ if (instance.wsConnection?.readyState === WebSocket.OPEN) {
970
+ // Check if last pong was too long ago (60s timeout)
971
+ if (instance.lastPong && Date.now() - instance.lastPong > 60000) {
972
+ this.logger.warn?.(`[VisualEditorBridge] Ping timeout for agent: ${agentId}`);
973
+ this._cleanupConnection(agentId);
974
+ this._scheduleReconnect(agentId);
975
+ return;
976
+ }
977
+
978
+ this.sendCommand(agentId, MessageTypes.PING, {});
979
+ }
980
+ }, 30000);
981
+ }
982
+
983
+ /**
984
+ * Graceful shutdown - stop all instances
985
+ */
986
+ async shutdown() {
987
+ this.logger.info?.('[VisualEditorBridge] Shutting down...');
988
+
989
+ // Stop cleanup interval
990
+ if (this.cleanupInterval) {
991
+ clearInterval(this.cleanupInterval);
992
+ this.cleanupInterval = null;
993
+ }
994
+
995
+ // Stop all instances
996
+ const stopPromises = [];
997
+ for (const agentId of this.instances.keys()) {
998
+ stopPromises.push(this.stopInstance(agentId));
999
+ }
1000
+
1001
+ await Promise.all(stopPromises);
1002
+
1003
+ this.logger.info?.('[VisualEditorBridge] Shutdown complete');
1004
+ this.emit('shutdown');
1005
+ }
1006
+ }
1007
+
1008
+ // Export singleton factory
1009
+ let bridgeInstance = null;
1010
+
1011
+ /**
1012
+ * Get or create the bridge singleton
1013
+ * @param {Object} config - Configuration (only used on first call)
1014
+ * @returns {VisualEditorBridge}
1015
+ */
1016
+ export function getVisualEditorBridge(config = {}) {
1017
+ if (!bridgeInstance) {
1018
+ bridgeInstance = new VisualEditorBridge(config);
1019
+ }
1020
+ return bridgeInstance;
1021
+ }
1022
+
1023
+ /**
1024
+ * Reset the singleton (for testing)
1025
+ */
1026
+ export function resetVisualEditorBridge() {
1027
+ if (bridgeInstance) {
1028
+ bridgeInstance.shutdown();
1029
+ bridgeInstance = null;
1030
+ }
1031
+ }
1032
+
1033
+ export default VisualEditorBridge;