@agenthippo-ai/agenthippo 0.1.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 (480) hide show
  1. package/LICENSE +21 -0
  2. package/bin/agenthippo.js +2 -0
  3. package/out/agentpack/github.js +547 -0
  4. package/out/agentpack/index.js +66 -0
  5. package/out/agentpack/install.js +297 -0
  6. package/out/agentpack/manager.js +441 -0
  7. package/out/agentpack/manifest.js +135 -0
  8. package/out/agentpack/paths.js +475 -0
  9. package/out/agentpack/publish.js +248 -0
  10. package/out/agentpack/stores.js +554 -0
  11. package/out/agentpack/types.js +5 -0
  12. package/out/analytics/dashboard/artifactManager.js +439 -0
  13. package/out/analytics/dashboard/fleetDisplayUtils.js +159 -0
  14. package/out/analytics/dashboard/fleetOrchestrator.js +142 -0
  15. package/out/analytics/dashboard/fleetRegistry.js +79 -0
  16. package/out/analytics/dashboard/fleetRowDefaults.js +87 -0
  17. package/out/analytics/dashboard/fleetScriptAdapter.js +146 -0
  18. package/out/analytics/dashboard/fleetStatusUtils.js +30 -0
  19. package/out/analytics/dashboard/fleetTitleUtils.js +68 -0
  20. package/out/analytics/dashboard/fleetTypes.js +29 -0
  21. package/out/analytics/dashboard/index.js +17 -0
  22. package/out/analytics/dashboard/indexingService.js +422 -0
  23. package/out/analytics/dashboard/localFleetAdapter.js +222 -0
  24. package/out/analytics/dashboard/performanceMonitor.js +76 -0
  25. package/out/analytics/dashboard/processManager.js +209 -0
  26. package/out/analytics/dashboard/provider.js +3885 -0
  27. package/out/analytics/dashboard/queryRunner.js +994 -0
  28. package/out/analytics/dashboard/refreshPolicy.js +16 -0
  29. package/out/analytics/dashboard/scheduledFleetFingerprint.js +22 -0
  30. package/out/analytics/dashboard/scheduledFleetStorage.js +241 -0
  31. package/out/analytics/dashboard/scheduledFleetUtils.js +22 -0
  32. package/out/analytics/dashboard/sessionEventFleetStats.js +118 -0
  33. package/out/analytics/dashboard/sessionJsonlFleet.js +204 -0
  34. package/out/analytics/dashboard/teamViewDiscovery.js +106 -0
  35. package/out/analytics/dashboard/viewsConfigMerge.js +221 -0
  36. package/out/analytics/dashboard/webview/analytics-panel.html +155 -0
  37. package/out/analytics/dashboard/webview/codicon.ttf +0 -0
  38. package/out/analytics/dashboard/webview/index.html +78 -0
  39. package/out/analytics/dashboard/webview/panel.js +2295 -0
  40. package/out/analytics/dashboard/webview/runtime-bridge.js +82 -0
  41. package/out/analytics/dashboard/webview/styles.css +1009 -0
  42. package/out/analytics/lifecycle.js +43 -0
  43. package/out/analytics/service.js +1279 -0
  44. package/out/analytics/sessionEvents.js +231 -0
  45. package/out/auth/bootstrapApply.js +72 -0
  46. package/out/auth/brokerClient.js +126 -0
  47. package/out/auth/callbackUri.js +59 -0
  48. package/out/auth/callbackUriParse.js +59 -0
  49. package/out/auth/githubStoreGate.js +70 -0
  50. package/out/auth/loopbackAuthServer.js +165 -0
  51. package/out/auth/pkce.js +9 -0
  52. package/out/auth/provider.js +239 -0
  53. package/out/auth/sessionStore.js +193 -0
  54. package/out/auth/signInFlow.js +182 -0
  55. package/out/auth/types.js +12 -0
  56. package/out/auth-broker/config.js +62 -0
  57. package/out/auth-broker/jwt.js +105 -0
  58. package/out/auth-broker/pkce.js +57 -0
  59. package/out/auth-broker/server.js +539 -0
  60. package/out/auth-broker/store.js +273 -0
  61. package/out/base-prompts/gateway/openclaw-starter.json5 +144 -0
  62. package/out/base-prompts/gateway/slack-socket-mode-minimal.manifest.json +44 -0
  63. package/out/base-prompts/mcp-servers/.mcp.json +13 -0
  64. package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +142 -0
  65. package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +13 -0
  66. package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +1192 -0
  67. package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +23 -0
  68. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +12 -0
  69. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +102 -0
  70. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +137 -0
  71. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +118 -0
  72. package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +209 -0
  73. package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +431 -0
  74. package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +270 -0
  75. package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +18 -0
  76. package/out/base-prompts/rules/AGENTS.md +204 -0
  77. package/out/base-prompts/skills/agent-browser/SKILL.md +102 -0
  78. package/out/base-prompts/skills/agent-builder/SKILL.md +73 -0
  79. package/out/base-prompts/skills/agent-builder/references/agent-pack.md +63 -0
  80. package/out/base-prompts/skills/agent-builder/references/crewai.md +66 -0
  81. package/out/base-prompts/skills/agent-builder/references/langchain.md +60 -0
  82. package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +73 -0
  83. package/out/base-prompts/skills/agent-engine-creator/SKILL.md +586 -0
  84. package/out/base-prompts/skills/agent-engine-installer/SKILL.md +106 -0
  85. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +259 -0
  86. package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +189 -0
  87. package/out/base-prompts/skills/agenthippo-cli/SKILL.md +69 -0
  88. package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +63 -0
  89. package/out/base-prompts/skills/agentpack-creator/SKILL.md +375 -0
  90. package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +261 -0
  91. package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +195 -0
  92. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +391 -0
  93. package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +294 -0
  94. package/out/base-prompts/skills/agentpack-testbench/SKILL.md +135 -0
  95. package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +202 -0
  96. package/out/base-prompts/skills/algorithmic-art/SKILL.md +405 -0
  97. package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +223 -0
  98. package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +599 -0
  99. package/out/base-prompts/skills/debug-agent/SKILL.md +83 -0
  100. package/out/base-prompts/skills/desktop-automation/SKILL.md +166 -0
  101. package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +441 -0
  102. package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +280 -0
  103. package/out/base-prompts/skills/docx/LICENSE.txt +30 -0
  104. package/out/base-prompts/skills/docx/SKILL.md +197 -0
  105. package/out/base-prompts/skills/docx/docx-js.md +350 -0
  106. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  107. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  108. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  109. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  110. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  111. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  112. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  113. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  114. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  115. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  116. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  117. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  118. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  119. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  120. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  121. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  122. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  123. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  124. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  125. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  126. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  127. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  128. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  129. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  130. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  131. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  132. package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  133. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  134. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  135. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  136. package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  137. package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
  138. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  139. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  140. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  141. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  142. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  143. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  144. package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  145. package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +159 -0
  146. package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +29 -0
  147. package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +69 -0
  148. package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
  149. package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +951 -0
  150. package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +274 -0
  151. package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
  152. package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
  153. package/out/base-prompts/skills/docx/ooxml.md +610 -0
  154. package/out/base-prompts/skills/docx/scripts/__init__.py +1 -0
  155. package/out/base-prompts/skills/docx/scripts/document.py +1276 -0
  156. package/out/base-prompts/skills/docx/scripts/templates/comments.xml +3 -0
  157. package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +3 -0
  158. package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  159. package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +3 -0
  160. package/out/base-prompts/skills/docx/scripts/templates/people.xml +3 -0
  161. package/out/base-prompts/skills/docx/scripts/utilities.py +374 -0
  162. package/out/base-prompts/skills/gateway-outbound/SKILL.md +136 -0
  163. package/out/base-prompts/skills/mcp-builder/LICENSE.txt +202 -0
  164. package/out/base-prompts/skills/mcp-builder/SKILL.md +328 -0
  165. package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +602 -0
  166. package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  167. package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  168. package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  169. package/out/base-prompts/skills/mcp-builder/scripts/connections.py +151 -0
  170. package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +373 -0
  171. package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  172. package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +2 -0
  173. package/out/base-prompts/skills/openclaw/SKILL.md +406 -0
  174. package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +61 -0
  175. package/out/base-prompts/skills/pdf/LICENSE.txt +30 -0
  176. package/out/base-prompts/skills/pdf/SKILL.md +294 -0
  177. package/out/base-prompts/skills/pdf/forms.md +205 -0
  178. package/out/base-prompts/skills/pdf/reference.md +612 -0
  179. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +70 -0
  180. package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
  181. package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +12 -0
  182. package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
  183. package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +41 -0
  184. package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +152 -0
  185. package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +114 -0
  186. package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  187. package/out/base-prompts/skills/pptx/LICENSE.txt +30 -0
  188. package/out/base-prompts/skills/pptx/SKILL.md +484 -0
  189. package/out/base-prompts/skills/pptx/html2pptx.md +625 -0
  190. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  191. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  192. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  193. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  194. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  195. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  196. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  197. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  198. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  199. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  200. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  201. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  202. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  203. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  204. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  205. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  206. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  207. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  208. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  209. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  210. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  211. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  212. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  213. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  214. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  215. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  216. package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  217. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  218. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  219. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  220. package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  221. package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
  222. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  223. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  224. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  225. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  226. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  227. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  228. package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  229. package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +159 -0
  230. package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +29 -0
  231. package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +69 -0
  232. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
  233. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +951 -0
  234. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
  235. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
  236. package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
  237. package/out/base-prompts/skills/pptx/ooxml.md +427 -0
  238. package/out/base-prompts/skills/pptx/scripts/html2pptx.js +984 -0
  239. package/out/base-prompts/skills/pptx/scripts/inventory.py +1020 -0
  240. package/out/base-prompts/skills/pptx/scripts/rearrange.py +231 -0
  241. package/out/base-prompts/skills/pptx/scripts/replace.py +385 -0
  242. package/out/base-prompts/skills/pptx/scripts/thumbnail.py +450 -0
  243. package/out/base-prompts/skills/prompt-lab/SKILL.md +149 -0
  244. package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +2551 -0
  245. package/out/base-prompts/skills/skill-creator/LICENSE.txt +202 -0
  246. package/out/base-prompts/skills/skill-creator/SKILL.md +371 -0
  247. package/out/base-prompts/skills/skill-creator/references/output-patterns.md +82 -0
  248. package/out/base-prompts/skills/skill-creator/references/workflows.md +28 -0
  249. package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +303 -0
  250. package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +110 -0
  251. package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +95 -0
  252. package/out/base-prompts/skills/skill-installer/SKILL.md +251 -0
  253. package/out/base-prompts/skills/skill-installer/references/store-api.md +219 -0
  254. package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +83 -0
  255. package/out/base-prompts/skills/spotlight-insights/SKILL.md +101 -0
  256. package/out/base-prompts/skills/spotlight-optimize/SKILL.md +93 -0
  257. package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +2551 -0
  258. package/out/base-prompts/skills/spotlight-query/SKILL.md +314 -0
  259. package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +163 -0
  260. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +2551 -0
  261. package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +744 -0
  262. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +14 -0
  263. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +7 -0
  264. package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +5 -0
  265. package/out/base-prompts/skills/spotlight-report/SKILL.md +86 -0
  266. package/out/base-prompts/skills/spotlight-visualize/SKILL.md +92 -0
  267. package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +2551 -0
  268. package/out/base-prompts/skills/xlsx/LICENSE.txt +30 -0
  269. package/out/base-prompts/skills/xlsx/SKILL.md +289 -0
  270. package/out/base-prompts/skills/xlsx/recalc.py +178 -0
  271. package/out/branding.js +23 -0
  272. package/out/cli/commands/anywhere.js +66 -0
  273. package/out/cli/commands/ask.js +243 -0
  274. package/out/cli/commands/auth-broker.js +24 -0
  275. package/out/cli/commands/auth.js +219 -0
  276. package/out/cli/commands/chat.js +354 -0
  277. package/out/cli/commands/deploy.js +223 -0
  278. package/out/cli/commands/gateway.js +160 -0
  279. package/out/cli/commands/pack.js +220 -0
  280. package/out/cli/commands/probe-custom-engine.js +82 -0
  281. package/out/cli/commands/serve.js +583 -0
  282. package/out/cli/commands/store.js +919 -0
  283. package/out/cli/credential-context.js +113 -0
  284. package/out/cli/deploy-init.js +2087 -0
  285. package/out/cli/deps.js +238 -0
  286. package/out/cli/index.js +390 -0
  287. package/out/cli/pack-loader.js +376 -0
  288. package/out/cli/run-custom-engine-runner.js +318 -0
  289. package/out/cli/runtime.js +409 -0
  290. package/out/cli/serve-hardening.js +325 -0
  291. package/out/cli/state-sync.js +135 -0
  292. package/out/cli/utils.js +220 -0
  293. package/out/cli/vscode-shim.js +86 -0
  294. package/out/engines/base.js +159 -0
  295. package/out/engines/claude.js +1215 -0
  296. package/out/engines/codex.js +759 -0
  297. package/out/engines/custom.js +632 -0
  298. package/out/engines/customEngineIpc.js +95 -0
  299. package/out/engines/customEngineRunnerEmbed.js +107 -0
  300. package/out/engines/customRegistry.js +274 -0
  301. package/out/engines/embeddedCustomEngineRunner.generated.js +9 -0
  302. package/out/engines/engineApiKey.js +138 -0
  303. package/out/engines/factory.js +45 -0
  304. package/out/engines/install.js +372 -0
  305. package/out/engines/persistentEngineWorker.js +320 -0
  306. package/out/engines/publish.js +341 -0
  307. package/out/engines/types.js +17 -0
  308. package/out/examples/run-headless-repl.js +291 -0
  309. package/out/examples/run-headless.js +187 -0
  310. package/out/extension.js +4472 -0
  311. package/out/gateway/bridge.js +805 -0
  312. package/out/gateway/cli-agent-id.js +36 -0
  313. package/out/gateway/client.js +303 -0
  314. package/out/gateway/client.test.js +395 -0
  315. package/out/gateway/gateway-scope-errors.js +14 -0
  316. package/out/gateway/gatewayBackup.js +76 -0
  317. package/out/gateway/gatewayRouteIndex.js +371 -0
  318. package/out/gateway/gatewaySessionStore.js +243 -0
  319. package/out/gateway/index.js +31 -0
  320. package/out/gateway/lifecycle.js +3056 -0
  321. package/out/gateway/openclaw-channel-routing.js +317 -0
  322. package/out/gateway/outboundHandles.js +373 -0
  323. package/out/gateway/outboundPaths.js +72 -0
  324. package/out/gateway/outboundSend.js +254 -0
  325. package/out/gateway/protocol.js +48 -0
  326. package/out/gateway/resetMappings.js +52 -0
  327. package/out/gateway/service.js +1244 -0
  328. package/out/gateway/sessionRouting.js +77 -0
  329. package/out/headless/index.js +505 -0
  330. package/out/headless/sync.js +473 -0
  331. package/out/mcp/publish.js +143 -0
  332. package/out/mcp.js +597 -0
  333. package/out/permissions.js +108 -0
  334. package/out/permissions.mapping.yaml +68 -0
  335. package/out/registry/index.js +332 -0
  336. package/out/remote/handleRemoteChat.js +122 -0
  337. package/out/remote/remotePackStatusBar.js +103 -0
  338. package/out/remote/serveClient.js +216 -0
  339. package/out/remoteSkills.js +191 -0
  340. package/out/runtime/index.js +32 -0
  341. package/out/runtime/interfaces/config-provider.js +32 -0
  342. package/out/runtime/interfaces/edit-tracker.js +22 -0
  343. package/out/runtime/interfaces/index.js +29 -0
  344. package/out/runtime/interfaces/logger.js +40 -0
  345. package/out/runtime/interfaces/permission-handler.js +66 -0
  346. package/out/runtime/interfaces/session-store.js +342 -0
  347. package/out/runtime/vscode/index.js +20 -0
  348. package/out/runtime/vscode/vscode-config-provider.js +77 -0
  349. package/out/runtime/vscode/vscode-edit-tracker.js +120 -0
  350. package/out/runtime/vscode/vscode-logger.js +30 -0
  351. package/out/runtime/vscode/vscode-permission-handler.js +72 -0
  352. package/out/runtime/vscode/vscode-session-store.js +22 -0
  353. package/out/scripts/agentide-litellm.ps1 +501 -0
  354. package/out/scripts/agentide-litellm.sh +700 -0
  355. package/out/scripts/agentide_litellm_hooks_v1.py +604 -0
  356. package/out/scripts/analytics/agent-fleet-widget.js +954 -0
  357. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +160 -0
  358. package/out/scripts/analytics/default-seed-scripts.json +12 -0
  359. package/out/scripts/analytics/duckdbLocator.js +29 -0
  360. package/out/scripts/analytics/fleet-local-adapter.js +246 -0
  361. package/out/scripts/analytics/fleet-workflow.sh +263 -0
  362. package/out/scripts/analytics/generate-default-charts.py +685 -0
  363. package/out/scripts/analytics/hotspot-analysis-widget.js +909 -0
  364. package/out/scripts/analytics/index-traces.py +1394 -0
  365. package/out/scripts/analytics/install-analytics.sh +286 -0
  366. package/out/scripts/analytics/live-fleet-4-collector.cjs +315 -0
  367. package/out/scripts/analytics/live-fleet-4-daemon.mjs +134 -0
  368. package/out/scripts/analytics/live-fleet-4-widget.cjs +433 -0
  369. package/out/scripts/analytics/otel-collector.yaml +55 -0
  370. package/out/scripts/analytics/query-analytics.ps1 +396 -0
  371. package/out/scripts/analytics/query-analytics.sh +2551 -0
  372. package/out/scripts/analytics/session-comparison-widget.js +1468 -0
  373. package/out/scripts/analytics/session-timeline-widget.js +2137 -0
  374. package/out/scripts/analytics/setup-analytics.ps1 +432 -0
  375. package/out/scripts/analytics/setup-analytics.sh +854 -0
  376. package/out/scripts/analytics/start-analytics.ps1 +169 -0
  377. package/out/scripts/analytics/start-analytics.sh +323 -0
  378. package/out/scripts/analytics/token-cost-flamegraph-widget.js +954 -0
  379. package/out/scripts/analytics/token-cost-trend-widget.js +1062 -0
  380. package/out/scripts/analytics/workflows-collector.cjs +315 -0
  381. package/out/scripts/analytics/workflows-daemon.mjs +186 -0
  382. package/out/scripts/analytics/workflows-widget.cjs +433 -0
  383. package/out/scripts/analytics/wow-model-widget.js +191 -0
  384. package/out/scripts/gateway/Dockerfile.serve +45 -0
  385. package/out/scripts/gateway/docker-serve.sh +248 -0
  386. package/out/scripts/gateway/setup-gateway-server.ps1 +953 -0
  387. package/out/scripts/gateway/setup-gateway-server.sh +935 -0
  388. package/out/scripts/gateway/setup-gateway.ps1 +1813 -0
  389. package/out/scripts/gateway/setup-gateway.sh +1541 -0
  390. package/out/scripts/setup-cli.ps1 +86 -0
  391. package/out/scripts/setup-cli.sh +117 -0
  392. package/out/scripts/tools-manifest.json +83 -0
  393. package/out/scripts/vendor-tools.mjs +234 -0
  394. package/out/session.js +272 -0
  395. package/out/shared/agentEngines.js +170 -0
  396. package/out/shared/index.js +109 -0
  397. package/out/shared/models.js +235 -0
  398. package/out/shared/providers.js +202 -0
  399. package/out/skills/publish.js +178 -0
  400. package/out/spotlight/install.js +339 -0
  401. package/out/spotlight/publish.js +544 -0
  402. package/out/spotlight/yaml.js +31 -0
  403. package/out/store/github/setup.js +235 -0
  404. package/out/store/github/templates/build-index.ts +339 -0
  405. package/out/store/github/templates/build-index.yml +97 -0
  406. package/out/store/github/templates/package.json +11 -0
  407. package/out/store/index.js +755 -0
  408. package/out/store/oramaIndex.js +884 -0
  409. package/out/store/shared.js +241 -0
  410. package/out/sync.js +1827 -0
  411. package/out/terminalLogger.js +358 -0
  412. package/out/test/agentEnvFile.test.js +97 -0
  413. package/out/test/agentYaml.test.js +373 -0
  414. package/out/test/agentpack.test.js +639 -0
  415. package/out/test/analytics-lifecycle.test.js +174 -0
  416. package/out/test/analytics.test.js +524 -0
  417. package/out/test/auth-bootstrapApply.test.js +102 -0
  418. package/out/test/auth-broker.test.js +434 -0
  419. package/out/test/auth-callback.test.js +62 -0
  420. package/out/test/branding.test.js +229 -0
  421. package/out/test/cli-agent-id.test.js +55 -0
  422. package/out/test/credential-context.test.js +105 -0
  423. package/out/test/customEngineInstall.test.js +100 -0
  424. package/out/test/customRegistry.test.js +122 -0
  425. package/out/test/deploy-init.test.js +532 -0
  426. package/out/test/deps-auto-install.test.js +81 -0
  427. package/out/test/engine-integration.test.js +297 -0
  428. package/out/test/enginePaths.test.js +195 -0
  429. package/out/test/enginePublish.test.js +82 -0
  430. package/out/test/fleetDisplayUtils.test.js +96 -0
  431. package/out/test/gateway-client.test.js +381 -0
  432. package/out/test/gateway-lifecycle.test.js +608 -0
  433. package/out/test/gateway-outbound.test.js +531 -0
  434. package/out/test/gateway-reset-mappings.test.js +221 -0
  435. package/out/test/gateway-route-index.test.js +235 -0
  436. package/out/test/gateway-session-routing.test.js +47 -0
  437. package/out/test/gateway-session-store.test.js +216 -0
  438. package/out/test/github.test.js +232 -0
  439. package/out/test/headless-sync.test.js +279 -0
  440. package/out/test/helpers/manifest-fixtures.js +27 -0
  441. package/out/test/mcp-obo.test.js +82 -0
  442. package/out/test/mcp.test.js +307 -0
  443. package/out/test/openclaw-channel-routing.test.js +191 -0
  444. package/out/test/pathSafety.test.js +63 -0
  445. package/out/test/permissions.test.js +175 -0
  446. package/out/test/persistent-engine-worker.test.js +220 -0
  447. package/out/test/refreshPolicy.test.js +60 -0
  448. package/out/test/registry.test.js +351 -0
  449. package/out/test/runtime.test.js +349 -0
  450. package/out/test/scheduledFleetStorage.test.js +72 -0
  451. package/out/test/serve-hardening.test.js +192 -0
  452. package/out/test/serveClient.test.js +173 -0
  453. package/out/test/sessionJsonlFleet.test.js +78 -0
  454. package/out/test/shared.test.js +173 -0
  455. package/out/test/skills-mcp.test.js +299 -0
  456. package/out/test/spotlightPublish.test.js +299 -0
  457. package/out/test/spotlightYaml.test.js +58 -0
  458. package/out/test/state-sync-peer.test.js +74 -0
  459. package/out/test/store.test.js +331 -0
  460. package/out/test/stores.test.js +504 -0
  461. package/out/test/sync-switch.test.js +251 -0
  462. package/out/test/teamViewDiscovery.test.js +90 -0
  463. package/out/test/toolPath.test.js +120 -0
  464. package/out/test/utils.test.js +308 -0
  465. package/out/test/viewsConfigMerge.test.js +180 -0
  466. package/out/test/windows-command-line.test.js +75 -0
  467. package/out/utils/agentEnvFile.js +131 -0
  468. package/out/utils/binary.js +175 -0
  469. package/out/utils/index.js +37 -0
  470. package/out/utils/network.js +74 -0
  471. package/out/utils/pathSafety.js +60 -0
  472. package/out/utils/paths.js +116 -0
  473. package/out/utils/pid.js +180 -0
  474. package/out/utils/pythonRunner.js +258 -0
  475. package/out/utils/resolveNodeBinary.js +168 -0
  476. package/out/utils/toolPath.js +83 -0
  477. package/out/utils/windowsCommandLine.js +29 -0
  478. package/out/utils.js +915 -0
  479. package/out/workspaceFolders.js +59 -0
  480. package/package.json +41 -0
@@ -0,0 +1,3056 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.GatewayLifecycleManager = exports.AGENT_ANYWHERE_LABEL = void 0;
40
+ exports.detectGateway = detectGateway;
41
+ /* eslint-disable local/code-no-unexternalized-strings */
42
+ /**
43
+ * Agent Anywhere -- Gateway Lifecycle Manager
44
+ *
45
+ * Detects, installs, starts, stops, and monitors the OpenClaw Gateway process.
46
+ * All actions are opt-in -- nothing auto-installs or auto-starts without explicit user action.
47
+ *
48
+ * Branding: the user-facing feature is called "Agent Anywhere".
49
+ * Internals still use "gateway" for code references.
50
+ */
51
+ const vscode = __importStar(require("vscode"));
52
+ const child_process_1 = require("child_process");
53
+ const crypto_1 = require("crypto");
54
+ const util_1 = require("util");
55
+ const fs = __importStar(require("fs"));
56
+ const path = __importStar(require("path"));
57
+ const os = __importStar(require("os"));
58
+ const index_1 = require("../utils/index");
59
+ const agentEnvFile_js_1 = require("../utils/agentEnvFile.js");
60
+ const windowsCommandLine_js_1 = require("../utils/windowsCommandLine.js");
61
+ const gateway_scope_errors_js_1 = require("./gateway-scope-errors.js");
62
+ const openclaw_channel_routing_js_1 = require("./openclaw-channel-routing.js");
63
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
64
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
65
+ const IS_WINDOWS = process.platform === "win32";
66
+ const IS_MACOS = process.platform === "darwin";
67
+ function addPathEntries(acc, value) {
68
+ if (!value) {
69
+ return;
70
+ }
71
+ for (const entry of value.split(path.delimiter)) {
72
+ const trimmed = entry.trim();
73
+ if (trimmed.length > 0) {
74
+ acc.add(trimmed);
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Build a resilient PATH for setup terminals launched from signed macOS apps (Finder launch),
80
+ * where process.env.PATH can be minimal and miss Homebrew/nvm/pnpm locations.
81
+ */
82
+ function buildAnywhereSetupPath() {
83
+ const entries = new Set();
84
+ const home = os.homedir();
85
+ // Always keep core system paths first.
86
+ addPathEntries(entries, "/usr/bin:/bin:/usr/sbin:/sbin");
87
+ addPathEntries(entries, process.env.PATH);
88
+ const commonDirs = [
89
+ path.join(home, ".local", "bin"),
90
+ path.join(home, "Library", "pnpm"),
91
+ path.join(home, ".local", "share", "pnpm"),
92
+ path.join(home, ".npm-global", "bin"),
93
+ path.join(home, ".volta", "bin"),
94
+ path.join(home, ".fnm"),
95
+ path.join(home, ".asdf", "shims"),
96
+ "/opt/homebrew/bin",
97
+ "/usr/local/bin",
98
+ ];
99
+ for (const dir of commonDirs) {
100
+ if (fs.existsSync(dir)) {
101
+ entries.add(dir);
102
+ }
103
+ }
104
+ // Include available nvm Node bin dirs (newest first) if present.
105
+ const nvmDir = process.env.NVM_DIR || path.join(home, ".nvm");
106
+ const nvmVersionsDir = path.join(nvmDir, "versions", "node");
107
+ if (fs.existsSync(nvmVersionsDir)) {
108
+ try {
109
+ const versions = fs
110
+ .readdirSync(nvmVersionsDir, { withFileTypes: true })
111
+ .filter((d) => d.isDirectory())
112
+ .map((d) => d.name)
113
+ .sort()
114
+ .reverse()
115
+ .slice(0, 8);
116
+ for (const version of versions) {
117
+ const binDir = path.join(nvmVersionsDir, version, "bin");
118
+ if (fs.existsSync(binDir)) {
119
+ entries.add(binDir);
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Ignore nvm directory read errors; fallback PATH still works.
125
+ }
126
+ }
127
+ return Array.from(entries).join(path.delimiter);
128
+ }
129
+ function getAnywhereSetupShellPath() {
130
+ if (IS_MACOS && fs.existsSync("/bin/zsh")) {
131
+ return "/bin/zsh";
132
+ }
133
+ return "/bin/bash";
134
+ }
135
+ function getAnywhereSetupShellArgs(command) {
136
+ // macOS: use login+interactive zsh so ~/.zprofile and ~/.zshrc can expose node/npm/pnpm.
137
+ if (IS_MACOS) {
138
+ return ["-lic", command];
139
+ }
140
+ // Linux: login bash is enough for common system/profile PATH setups.
141
+ return ["-lc", command];
142
+ }
143
+ /**
144
+ * Env for gateway setup terminals: npm's lifecycle runner spawns bare `sh` unless script-shell
145
+ * is set; @npmcli/run-script can rebuild PATH without /bin when the parent PATH is empty (IDE hosts).
146
+ */
147
+ function anywhereSetupTerminalEnv() {
148
+ if (IS_WINDOWS) {
149
+ return undefined;
150
+ }
151
+ return {
152
+ ...process.env,
153
+ PATH: buildAnywhereSetupPath(),
154
+ npm_config_script_shell: "/bin/sh",
155
+ };
156
+ }
157
+ // ============================================================================
158
+ // Constants
159
+ // ============================================================================
160
+ /** User-facing feature name */
161
+ exports.AGENT_ANYWHERE_LABEL = "Agent Anywhere";
162
+ const AGENT_ANYWHERE_SETUP_TERMINAL = `${exports.AGENT_ANYWHERE_LABEL} Setup`;
163
+ const AGENT_ANYWHERE_UNINSTALL_TERMINAL = `${exports.AGENT_ANYWHERE_LABEL} Uninstall`;
164
+ /** Default gateway port */
165
+ const DEFAULT_PORT = 18789;
166
+ /** Persistent gateway stdout/stderr log (see doStart). */
167
+ const DEFAULT_GATEWAY_LOG_PATH = path.join(os.homedir(), ".openclaw", "gateway.log");
168
+ const OPEN_GATEWAY_LOG_ACTION = "Open Gateway Log";
169
+ function stripAnsi(text) {
170
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
171
+ }
172
+ /** Last actionable error line from gateway.log for startup failure toasts. */
173
+ function readLastGatewayLogHint(logPath) {
174
+ try {
175
+ if (!fs.existsSync(logPath)) {
176
+ return undefined;
177
+ }
178
+ const content = fs.readFileSync(logPath, "utf8");
179
+ const lines = content.split(/\r?\n/).filter((line) => line.trim());
180
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 50); i--) {
181
+ const plain = stripAnsi(lines[i]).trim();
182
+ if (!plain || /Doctor warnings|force: no listeners/i.test(plain)) {
183
+ continue;
184
+ }
185
+ const gatewayFailed = plain.match(/Gateway failed to start:\s*(.+)/i);
186
+ if (gatewayFailed?.[1]) {
187
+ return gatewayFailed[1].trim().slice(0, 160);
188
+ }
189
+ if (/SecretRefResolutionError|is missing or empty|required secrets are unavailable/i.test(plain)) {
190
+ return plain.replace(/^\d{4}-\d{2}-\d{2}T[\d:.+-]+\s*/, "").slice(0, 160);
191
+ }
192
+ if (/errorCode=|errorMessage=/i.test(plain)) {
193
+ return plain.slice(0, 160);
194
+ }
195
+ }
196
+ }
197
+ catch {
198
+ // ignore read errors
199
+ }
200
+ return undefined;
201
+ }
202
+ async function showGatewayStartFailedToast(logPath) {
203
+ const hint = readLastGatewayLogHint(logPath);
204
+ const message = hint
205
+ ? `${exports.AGENT_ANYWHERE_LABEL}: Gateway failed to start. ${hint}`
206
+ : `${exports.AGENT_ANYWHERE_LABEL}: Gateway failed to start.`;
207
+ const choice = await vscode.window.showErrorMessage(message, OPEN_GATEWAY_LOG_ACTION);
208
+ if (choice === OPEN_GATEWAY_LOG_ACTION) {
209
+ try {
210
+ const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(logPath));
211
+ await vscode.window.showTextDocument(doc, { preview: false });
212
+ }
213
+ catch (err) {
214
+ const detail = err instanceof Error ? err.message : String(err);
215
+ void vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not open gateway log (${detail}). Path: ${logPath}`);
216
+ }
217
+ }
218
+ }
219
+ /** Health check interval in ms */
220
+ const HEALTH_CHECK_INTERVAL_MS = 30_000;
221
+ /** Gateway startup timeout in ms (Windows cold starts can take longer). */
222
+ const STARTUP_TIMEOUT_MS = IS_WINDOWS ? 75_000 : 20_000;
223
+ /**
224
+ * Only remove a gateway lock file when it is at least this old. Avoids a TOCTOU race where
225
+ * concurrent starts see "lock exists, port not listening yet" and delete another instance's fresh lock.
226
+ */
227
+ const STALE_GATEWAY_LOCK_MIN_AGE_MS = 30_000;
228
+ /** PID file for fast status checks (~1-2ms vs 100ms+ for shell commands) */
229
+ const GATEWAY_PID_FILE = index_1.PID_FILES.gateway();
230
+ const FORK_CLI_NAME = "hippoclaw";
231
+ const MANAGED_GATEWAY_STATE_DIR = path.join(os.homedir(), ".agenthippo", "gateway");
232
+ const MANAGED_GATEWAY_MANIFEST_PATH = path.join(MANAGED_GATEWAY_STATE_DIR, "install.json");
233
+ const GATEWAY_INSTALL_STATUS_FILE = path.join(MANAGED_GATEWAY_STATE_DIR, "install-status.json");
234
+ const MANAGED_GATEWAY_WRAPPER_DIR = IS_WINDOWS
235
+ ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "AgentHippo", "bin")
236
+ : path.join(os.homedir(), ".local", "bin");
237
+ const MANAGED_GATEWAY_WRAPPER_PATH = path.join(MANAGED_GATEWAY_WRAPPER_DIR, IS_WINDOWS ? "hippoclaw.cmd" : "hippoclaw");
238
+ const MANAGED_OPENCLAW_WRAPPER_PATH = path.join(MANAGED_GATEWAY_WRAPPER_DIR, IS_WINDOWS ? "openclaw.cmd" : "openclaw");
239
+ const MANAGED_GATEWAY_WRAPPER_MARKER = IS_WINDOWS
240
+ ? "REM Managed by Agent Anywhere"
241
+ : "# Managed by Agent Anywhere";
242
+ function installSourceNeedsBuild(source) {
243
+ if (!source) {
244
+ return false;
245
+ }
246
+ return (source.startsWith("\\") ||
247
+ source.startsWith("/") ||
248
+ source.startsWith(".") ||
249
+ /^[A-Za-z]:/.test(source) ||
250
+ source.startsWith("github:") ||
251
+ source.startsWith("git+http:") ||
252
+ source.startsWith("git+https:") ||
253
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.+)?$/.test(source));
254
+ }
255
+ const GATEWAY_INSTALL_PROGRESS_STEPS = [
256
+ ["npmInstallComplete", "npm install complete"],
257
+ ["wrapperExists", "wrapper exists"],
258
+ ["configExists", "config exists"],
259
+ ["distExists", "dist exists"],
260
+ ["webUiExists", "web UI exists"],
261
+ ["finalCheckPassed", "final check passed"],
262
+ ];
263
+ // ============================================================================
264
+ // Detection
265
+ // ============================================================================
266
+ /**
267
+ * Detect whether OpenClaw is installed and whether the gateway is running.
268
+ * This is a fast, non-destructive check.
269
+ *
270
+ * Detection logic:
271
+ * - If the managed fork CLI (`hippoclaw`) resolves and is usable, it's installed.
272
+ * - If the port is open, listener ownership is verified before it is treated as running.
273
+ * - We also check common install locations in case PATH is incomplete
274
+ * (VS Code's shell PATH often differs from the user's terminal).
275
+ */
276
+ function normalizePathForCompare(value) {
277
+ return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
278
+ }
279
+ function pathStartsWithPath(child, parent) {
280
+ const childNorm = normalizePathForCompare(child);
281
+ const parentNorm = normalizePathForCompare(parent);
282
+ return childNorm === parentNorm || childNorm.startsWith(`${parentNorm}/`);
283
+ }
284
+ function readManagedGatewayInstallRecord() {
285
+ try {
286
+ if (!fs.existsSync(MANAGED_GATEWAY_MANIFEST_PATH)) {
287
+ return undefined;
288
+ }
289
+ // Strip UTF-8 BOM — PowerShell writes JSON with BOM by default
290
+ const raw = fs.readFileSync(MANAGED_GATEWAY_MANIFEST_PATH, "utf8").replace(/^/, "");
291
+ return JSON.parse(raw);
292
+ }
293
+ catch {
294
+ return undefined;
295
+ }
296
+ }
297
+ function writeManagedGatewayInstallRecord(record) {
298
+ fs.mkdirSync(MANAGED_GATEWAY_STATE_DIR, { recursive: true });
299
+ fs.writeFileSync(MANAGED_GATEWAY_MANIFEST_PATH, JSON.stringify(record, null, 2), "utf8");
300
+ }
301
+ function getManagedGatewayPackageRoot(record) {
302
+ const candidate = record?.packageRoot;
303
+ if (!candidate || !fs.existsSync(candidate)) {
304
+ return undefined;
305
+ }
306
+ try {
307
+ return fs.realpathSync(candidate);
308
+ }
309
+ catch {
310
+ return candidate;
311
+ }
312
+ }
313
+ function resolveManagedGatewayCliTarget(binaryPath) {
314
+ if (!binaryPath) {
315
+ return undefined;
316
+ }
317
+ const record = readManagedGatewayInstallRecord();
318
+ if (!record?.entryScriptPath && !record?.cliPath) {
319
+ return binaryPath;
320
+ }
321
+ if ((record.wrapperPath || record.openclawWrapperPath) &&
322
+ [
323
+ record.wrapperPath,
324
+ record.openclawWrapperPath,
325
+ ].some((wrapperPath) => wrapperPath &&
326
+ normalizePathForCompare(binaryPath) === normalizePathForCompare(wrapperPath)) &&
327
+ ((record.entryScriptPath && fs.existsSync(record.entryScriptPath)) ||
328
+ (record.cliPath && fs.existsSync(record.cliPath)))) {
329
+ return record.entryScriptPath && fs.existsSync(record.entryScriptPath)
330
+ ? record.entryScriptPath
331
+ : record.cliPath;
332
+ }
333
+ return binaryPath;
334
+ }
335
+ function resolveManagedGatewayLauncherPath(binaryPath) {
336
+ if (!binaryPath) {
337
+ return undefined;
338
+ }
339
+ const record = readManagedGatewayInstallRecord();
340
+ if (!record?.cliPath) {
341
+ return binaryPath;
342
+ }
343
+ if ([
344
+ record.wrapperPath,
345
+ record.openclawWrapperPath,
346
+ record.entryScriptPath,
347
+ ].some((candidate) => candidate &&
348
+ normalizePathForCompare(binaryPath) === normalizePathForCompare(candidate)) &&
349
+ fs.existsSync(record.cliPath)) {
350
+ return record.cliPath;
351
+ }
352
+ return binaryPath;
353
+ }
354
+ function getManagedGatewayBinary(record) {
355
+ for (const candidate of [record?.wrapperPath, record?.openclawWrapperPath, record?.cliPath]) {
356
+ if (candidate && fs.existsSync(candidate)) {
357
+ return candidate;
358
+ }
359
+ }
360
+ return undefined;
361
+ }
362
+ function getManagedGatewayNodePath(record) {
363
+ const candidate = record?.nodePath;
364
+ if (!candidate || !fs.existsSync(candidate)) {
365
+ return undefined;
366
+ }
367
+ return candidate;
368
+ }
369
+ function getManagedGatewayEntryScriptPath(record) {
370
+ const candidate = record?.entryScriptPath;
371
+ if (!candidate || !fs.existsSync(candidate)) {
372
+ return undefined;
373
+ }
374
+ return candidate;
375
+ }
376
+ async function resolveManagedGatewayRuntimeSpec(binaryPath, record) {
377
+ const installRecord = record ?? readManagedGatewayInstallRecord();
378
+ const cachedBinary = binaryPath ?? getManagedGatewayBinary(installRecord) ?? (await getCachedBinaryPath());
379
+ const launcherPath = resolveManagedGatewayLauncherPath(cachedBinary) ?? cachedBinary;
380
+ if (!launcherPath || !fs.existsSync(launcherPath)) {
381
+ return undefined;
382
+ }
383
+ const packageRoot = getManagedGatewayPackageRoot(installRecord) ?? resolvePackageRootFromBinary(launcherPath);
384
+ if (!packageRoot) {
385
+ return undefined;
386
+ }
387
+ const nodePath = getManagedGatewayNodePath(installRecord) ?? resolveManagedGatewayNodePath(launcherPath);
388
+ const entryScriptPath = getManagedGatewayEntryScriptPath(installRecord) ??
389
+ resolveManagedGatewayEntryScriptFromPackageRoot(packageRoot);
390
+ if (!nodePath || !entryScriptPath) {
391
+ return undefined;
392
+ }
393
+ return {
394
+ launcherPath,
395
+ packageRoot,
396
+ nodePath,
397
+ entryScriptPath,
398
+ };
399
+ }
400
+ function isAgentAnywhereManagedWrapper(wrapperPath) {
401
+ try {
402
+ if (!fs.existsSync(wrapperPath)) {
403
+ return false;
404
+ }
405
+ const content = fs.readFileSync(wrapperPath, "utf8");
406
+ return content.includes(MANAGED_GATEWAY_WRAPPER_MARKER);
407
+ }
408
+ catch {
409
+ return false;
410
+ }
411
+ }
412
+ function writeManagedGatewayWrapper(nodePath, entryScriptPath, wrapperPath) {
413
+ fs.mkdirSync(path.dirname(wrapperPath), { recursive: true });
414
+ const content = IS_WINDOWS
415
+ ? `@echo off\r\n${MANAGED_GATEWAY_WRAPPER_MARKER}\r\n"${nodePath}" "${entryScriptPath}" %*\r\n`
416
+ : `#!/bin/sh\n${MANAGED_GATEWAY_WRAPPER_MARKER}\nexec "${nodePath}" "${entryScriptPath}" "$@"\n`;
417
+ fs.writeFileSync(wrapperPath, content, IS_WINDOWS ? "ascii" : "utf8");
418
+ if (!IS_WINDOWS) {
419
+ fs.chmodSync(wrapperPath, 0o755);
420
+ }
421
+ return wrapperPath;
422
+ }
423
+ function resolveManagedGatewayEntryScriptFromPackageRoot(packageRoot) {
424
+ if (!packageRoot) {
425
+ return undefined;
426
+ }
427
+ try {
428
+ const pkgJsonPath = path.join(packageRoot, "package.json");
429
+ if (!fs.existsSync(pkgJsonPath)) {
430
+ return undefined;
431
+ }
432
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
433
+ let bins = pkg.bin;
434
+ if (typeof bins === "string") {
435
+ bins = { [pkg.name || "openclaw"]: bins };
436
+ }
437
+ const orderedNames = [...new Set(["hippoclaw", "openclaw", ...Object.keys(bins || {})])];
438
+ for (const name of orderedNames) {
439
+ const relativeEntry = bins && typeof bins[name] === "string" ? bins[name] : undefined;
440
+ if (!relativeEntry) {
441
+ continue;
442
+ }
443
+ const candidate = path.resolve(packageRoot, relativeEntry);
444
+ if (fs.existsSync(candidate)) {
445
+ return candidate;
446
+ }
447
+ }
448
+ }
449
+ catch {
450
+ // ignore
451
+ }
452
+ return undefined;
453
+ }
454
+ function resolveManagedGatewayNodePath(binaryPath) {
455
+ const launcherPath = resolveManagedGatewayLauncherPath(binaryPath) ?? binaryPath;
456
+ if (launcherPath) {
457
+ const candidate = path.join(path.dirname(launcherPath), IS_WINDOWS ? "node.exe" : "node");
458
+ if (fs.existsSync(candidate)) {
459
+ return candidate;
460
+ }
461
+ }
462
+ for (const candidate of (0, index_1.getCommonBinaryPaths)("node")) {
463
+ if (fs.existsSync(candidate)) {
464
+ return candidate;
465
+ }
466
+ }
467
+ return undefined;
468
+ }
469
+ function removeManagedGatewayWrapperState() {
470
+ for (const wrapperPath of [MANAGED_GATEWAY_WRAPPER_PATH, MANAGED_OPENCLAW_WRAPPER_PATH]) {
471
+ if (isAgentAnywhereManagedWrapper(wrapperPath)) {
472
+ try {
473
+ fs.rmSync(wrapperPath, { force: true });
474
+ }
475
+ catch {
476
+ // ignore cleanup errors
477
+ }
478
+ }
479
+ }
480
+ try {
481
+ fs.rmSync(MANAGED_GATEWAY_MANIFEST_PATH, { force: true });
482
+ }
483
+ catch {
484
+ // ignore cleanup errors
485
+ }
486
+ }
487
+ function addManagedOpenclawRoot(roots, candidate) {
488
+ if (!fs.existsSync(candidate)) {
489
+ return;
490
+ }
491
+ try {
492
+ roots.add(fs.realpathSync(candidate));
493
+ }
494
+ catch {
495
+ roots.add(candidate);
496
+ }
497
+ }
498
+ /** Scan nvm version dirs when `npm root -g` is unavailable in the extension host PATH. */
499
+ function collectNvmOpenclawPackageRoots() {
500
+ const home = os.homedir();
501
+ const nvmDir = process.env.NVM_DIR || path.join(home, ".nvm");
502
+ const versionsDir = path.join(nvmDir, "versions", "node");
503
+ if (!fs.existsSync(versionsDir)) {
504
+ return [];
505
+ }
506
+ const candidates = [];
507
+ try {
508
+ for (const entry of fs.readdirSync(versionsDir, { withFileTypes: true })) {
509
+ if (!entry.isDirectory()) {
510
+ continue;
511
+ }
512
+ candidates.push(path.join(versionsDir, entry.name, "lib", "node_modules", "openclaw"));
513
+ }
514
+ }
515
+ catch {
516
+ // ignore nvm directory read errors
517
+ }
518
+ return candidates;
519
+ }
520
+ async function getManagedOpenclawRoots() {
521
+ const roots = new Set();
522
+ const manifestRecord = readManagedGatewayInstallRecord();
523
+ const manifestRoot = getManagedGatewayPackageRoot(manifestRecord);
524
+ if (manifestRoot && fs.existsSync(manifestRoot)) {
525
+ roots.add(manifestRoot);
526
+ }
527
+ const bootstrapNodeRoot = path.join(os.homedir(), ".agenthippo", "tools", "node");
528
+ if (fs.existsSync(bootstrapNodeRoot)) {
529
+ try {
530
+ const versions = fs.readdirSync(bootstrapNodeRoot, { withFileTypes: true });
531
+ for (const entry of versions) {
532
+ if (!entry.isDirectory()) {
533
+ continue;
534
+ }
535
+ // Windows npm uses node_modules/ directly (no lib/ prefix); Unix uses lib/node_modules/
536
+ const candidate = path.join(bootstrapNodeRoot, entry.name, ...(IS_WINDOWS ? ["node_modules"] : ["lib", "node_modules"]), "openclaw");
537
+ if (!fs.existsSync(candidate)) {
538
+ continue;
539
+ }
540
+ try {
541
+ roots.add(fs.realpathSync(candidate));
542
+ }
543
+ catch {
544
+ roots.add(candidate);
545
+ }
546
+ }
547
+ }
548
+ catch {
549
+ // ignore bootstrap node scan failures
550
+ }
551
+ }
552
+ // Windows npm uses node_modules/ directly (no lib/ prefix); Unix uses lib/node_modules/
553
+ const legacyManaged = path.join(os.homedir(), ".npm-global", ...(IS_WINDOWS ? ["node_modules"] : ["lib", "node_modules"]), "openclaw");
554
+ addManagedOpenclawRoot(roots, legacyManaged);
555
+ for (const nvmManaged of collectNvmOpenclawPackageRoots()) {
556
+ addManagedOpenclawRoot(roots, nvmManaged);
557
+ }
558
+ try {
559
+ const { stdout } = await execAsync("npm root -g", { timeout: 3_000 });
560
+ const npmRoot = stdout.trim();
561
+ if (npmRoot) {
562
+ addManagedOpenclawRoot(roots, path.join(npmRoot, "openclaw"));
563
+ }
564
+ }
565
+ catch {
566
+ // ignore npm root failures
567
+ }
568
+ return Array.from(roots);
569
+ }
570
+ function resolvePackageRootFromBinary(binaryPath) {
571
+ try {
572
+ const targetBinary = resolveManagedGatewayCliTarget(binaryPath) ?? binaryPath;
573
+ const realBinary = fs.realpathSync(targetBinary);
574
+ let pkgRoot = path.dirname(realBinary);
575
+ for (let i = 0; i < 8; i++) {
576
+ if (fs.existsSync(path.join(pkgRoot, "package.json"))) {
577
+ return pkgRoot;
578
+ }
579
+ const parent = path.dirname(pkgRoot);
580
+ if (parent === pkgRoot) {
581
+ break;
582
+ }
583
+ pkgRoot = parent;
584
+ }
585
+ }
586
+ catch {
587
+ // ignore
588
+ }
589
+ return undefined;
590
+ }
591
+ async function isManagedForkBinaryPath(binaryPath, managedRoots) {
592
+ const roots = managedRoots ?? (await getManagedOpenclawRoots());
593
+ if (roots.length === 0) {
594
+ return false;
595
+ }
596
+ const pkgRoot = resolvePackageRootFromBinary(binaryPath);
597
+ if (!pkgRoot) {
598
+ return false;
599
+ }
600
+ return roots.some((root) => pathStartsWithPath(pkgRoot, root));
601
+ }
602
+ async function getListeningCommandLinesForPort(port) {
603
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
604
+ return [];
605
+ }
606
+ if (IS_WINDOWS) {
607
+ try {
608
+ const command = `$listenerPids=Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ` +
609
+ `Select-Object -ExpandProperty OwningProcess -Unique; ` +
610
+ `foreach($listenerProcId in $listenerPids){$proc=Get-CimInstance Win32_Process -Filter \\\"ProcessId=$listenerProcId\\\" -ErrorAction SilentlyContinue; if($proc -and $proc.CommandLine){$proc.CommandLine}}`;
611
+ const { stdout } = await execAsync(`powershell.exe -NoProfile -Command "${command}"`, {
612
+ timeout: 10_000,
613
+ });
614
+ return stdout
615
+ .split(/\r?\n/)
616
+ .map((line) => line.trim())
617
+ .filter((line) => line.length > 0);
618
+ }
619
+ catch {
620
+ return [];
621
+ }
622
+ }
623
+ try {
624
+ const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true`, { timeout: 5_000 });
625
+ const pids = [
626
+ ...new Set(stdout
627
+ .trim()
628
+ .split(/\n/)
629
+ .map((s) => s.trim())
630
+ .filter((s) => /^\d+$/.test(s))),
631
+ ];
632
+ const commands = [];
633
+ for (const pid of pids) {
634
+ try {
635
+ const { stdout: cmdOut } = await execAsync(`ps -p ${pid} -o command=`, {
636
+ timeout: 3_000,
637
+ });
638
+ const cmd = cmdOut.trim();
639
+ if (cmd) {
640
+ commands.push(cmd);
641
+ }
642
+ }
643
+ catch {
644
+ // ignore per-pid failures
645
+ }
646
+ }
647
+ return commands;
648
+ }
649
+ catch {
650
+ return [];
651
+ }
652
+ }
653
+ async function isTrustedGatewayListener(port, binaryPath) {
654
+ const managedRoots = await getManagedOpenclawRoots();
655
+ if (managedRoots.length === 0) {
656
+ return false;
657
+ }
658
+ let binaryIsManaged = false;
659
+ let binaryReal = "";
660
+ if (binaryPath) {
661
+ binaryIsManaged = await isManagedForkBinaryPath(binaryPath, managedRoots);
662
+ if (binaryIsManaged) {
663
+ try {
664
+ binaryReal = fs.realpathSync(binaryPath);
665
+ }
666
+ catch {
667
+ binaryReal = binaryPath;
668
+ }
669
+ }
670
+ }
671
+ const commandLines = await getListeningCommandLinesForPort(port);
672
+ if (commandLines.length === 0) {
673
+ return false;
674
+ }
675
+ // The gateway sets process.title to "openclaw-gateway", so ps output won't
676
+ // contain file paths. Add the managed node binary path to both the command
677
+ // list (via lsof) and a matching needle, so we can trust processes started
678
+ // from our bootstrapped node tree even when the process title is opaque.
679
+ const manifestRecord = readManagedGatewayInstallRecord();
680
+ const manifestNodePath = getManagedGatewayNodePath(manifestRecord);
681
+ const augmentedCommands = [...commandLines];
682
+ if (manifestNodePath) {
683
+ augmentedCommands.push(manifestNodePath);
684
+ }
685
+ const rootNeedles = managedRoots.map((root) => normalizePathForCompare(root));
686
+ // Add the managed node binary dir as a needle so it can match against the
687
+ // augmented command list entry for the node executable.
688
+ if (manifestNodePath) {
689
+ rootNeedles.push(normalizePathForCompare(path.dirname(manifestNodePath)));
690
+ }
691
+ const binaryNeedle = binaryReal ? normalizePathForCompare(binaryReal) : "";
692
+ for (const cmd of augmentedCommands) {
693
+ const normalized = normalizePathForCompare(cmd);
694
+ if (normalized.includes(FORK_CLI_NAME)) {
695
+ if (binaryIsManaged) {
696
+ return true;
697
+ }
698
+ }
699
+ if (binaryNeedle && normalized.includes(binaryNeedle)) {
700
+ return true;
701
+ }
702
+ if (rootNeedles.some((needle) => normalized.includes(needle))) {
703
+ return true;
704
+ }
705
+ }
706
+ return false;
707
+ }
708
+ async function detectGateway(port = DEFAULT_PORT) {
709
+ // Parallelize port probe and binary lookup for ~50% faster detection
710
+ const [portOpen, managedRoots] = await Promise.all([
711
+ (0, index_1.probePort)(port),
712
+ getManagedOpenclawRoots(),
713
+ ]);
714
+ const binaryPath = await getCachedBinaryPath(managedRoots);
715
+ let version;
716
+ if (binaryPath) {
717
+ try {
718
+ const { stdout } = await execAsync(`"${binaryPath}" --version`, {
719
+ timeout: 3_000, // Reduced from 5s - local command should be fast
720
+ });
721
+ version = stdout.trim().split("\n")[0];
722
+ }
723
+ catch {
724
+ // Binary exists but --version failed
725
+ }
726
+ }
727
+ // Treat installed as "managed fork CLI is available and usable".
728
+ const installed = !!binaryPath && !!version;
729
+ const running = portOpen
730
+ ? await isTrustedGatewayListener(port, binaryPath)
731
+ : false;
732
+ return {
733
+ installed,
734
+ binaryPath,
735
+ version,
736
+ running,
737
+ port,
738
+ };
739
+ }
740
+ /**
741
+ * Ask the OpenClaw CLI to stop a supervised or local gateway (launchd/systemd aware).
742
+ */
743
+ async function tryOpenclawGatewayStopCli(binaryPath, log) {
744
+ try {
745
+ if (IS_WINDOWS) {
746
+ const runtimeSpec = await resolveManagedGatewayRuntimeSpec(binaryPath);
747
+ if (runtimeSpec) {
748
+ log("Running gateway stop via managed node runtime...");
749
+ await execFileAsync(runtimeSpec.nodePath, [runtimeSpec.entryScriptPath, "gateway", "stop"], { timeout: 20_000, windowsHide: true });
750
+ return;
751
+ }
752
+ }
753
+ log(`Running ${path.basename(binaryPath)} gateway stop (CLI)...`);
754
+ await execAsync(`"${binaryPath}" gateway stop`, { timeout: 20_000, windowsHide: true });
755
+ }
756
+ catch {
757
+ // Non-fatal — fall back to process signals / port-based kill
758
+ }
759
+ }
760
+ function readGatewayInstallStatusSnapshot() {
761
+ try {
762
+ if (!fs.existsSync(GATEWAY_INSTALL_STATUS_FILE)) {
763
+ return undefined;
764
+ }
765
+ const raw = fs.readFileSync(GATEWAY_INSTALL_STATUS_FILE, "utf8").replace(/^\uFEFF/, "");
766
+ return JSON.parse(raw);
767
+ }
768
+ catch {
769
+ return undefined;
770
+ }
771
+ }
772
+ function isGatewayInstallComplete(status) {
773
+ return status?.phase === "complete" && status.steps?.finalCheckPassed === true;
774
+ }
775
+ function getGatewayInstallProgressSummary(status) {
776
+ if (!status) {
777
+ return {
778
+ message: "Waiting for setup status...",
779
+ percent: 0,
780
+ };
781
+ }
782
+ if (status.phase === "failed") {
783
+ return {
784
+ message: status.lastError?.trim() || status.message?.trim() || "Install failed.",
785
+ percent: 100,
786
+ };
787
+ }
788
+ const completedLabels = GATEWAY_INSTALL_PROGRESS_STEPS
789
+ .filter(([key]) => status.steps?.[key])
790
+ .map(([, label]) => label);
791
+ const completedCount = completedLabels.length;
792
+ const phaseMessage = status.message?.trim() || "Installing Agent Anywhere...";
793
+ const progressMessage = completedLabels.length > 0
794
+ ? `${phaseMessage} (done: ${completedLabels.join(", ")})`
795
+ : phaseMessage;
796
+ return {
797
+ message: progressMessage,
798
+ percent: Math.min(95, Math.floor((completedCount / GATEWAY_INSTALL_PROGRESS_STEPS.length) * 100)),
799
+ };
800
+ }
801
+ function escapePowerShellSingleQuotedString(value) {
802
+ return value.replace(/'/g, "''");
803
+ }
804
+ async function getListeningProcessIdsForPort(port) {
805
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
806
+ return [];
807
+ }
808
+ if (IS_WINDOWS) {
809
+ try {
810
+ const { stdout } = await execAsync(`powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $_.OwningProcess } | Sort-Object -Unique"`, { timeout: 10_000, windowsHide: true });
811
+ return [
812
+ ...new Set(stdout
813
+ .trim()
814
+ .split(/\r?\n/)
815
+ .map((s) => parseInt(s.trim(), 10))
816
+ .filter((pid) => Number.isInteger(pid) && pid > 0)),
817
+ ];
818
+ }
819
+ catch {
820
+ return [];
821
+ }
822
+ }
823
+ try {
824
+ const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true`, { timeout: 5_000 });
825
+ return [
826
+ ...new Set(stdout
827
+ .trim()
828
+ .split(/\n/)
829
+ .map((s) => parseInt(s.trim(), 10))
830
+ .filter((pid) => Number.isInteger(pid) && pid > 0)),
831
+ ];
832
+ }
833
+ catch {
834
+ return [];
835
+ }
836
+ }
837
+ async function waitForGatewayShutdown(port, observedPids, timeoutMs = 8_000) {
838
+ const trackedPids = [...new Set(observedPids.filter((pid) => Number.isInteger(pid) && pid > 0))];
839
+ const deadline = Date.now() + timeoutMs;
840
+ let stableClosedSince = 0;
841
+ while (Date.now() < deadline) {
842
+ const portOpen = await (0, index_1.probePort)(port);
843
+ const stillRunning = trackedPids.filter((pid) => (0, index_1.isProcessRunning)(pid));
844
+ if (!portOpen && stillRunning.length === 0) {
845
+ if (stableClosedSince === 0) {
846
+ stableClosedSince = Date.now();
847
+ }
848
+ if (Date.now() - stableClosedSince >= 750) {
849
+ return true;
850
+ }
851
+ }
852
+ else {
853
+ stableClosedSince = 0;
854
+ }
855
+ await new Promise((resolve) => setTimeout(resolve, 250));
856
+ }
857
+ return !(await (0, index_1.probePort)(port)) && trackedPids.every((pid) => !(0, index_1.isProcessRunning)(pid));
858
+ }
859
+ /**
860
+ * Signal the gateway process group. We spawn the gateway launcher detached, so it is a process
861
+ * group leader and its listening child shares the group; signalling the group avoids orphaning the
862
+ * child that actually holds the port. Falls back to signalling just the launcher when the group
863
+ * signal is unavailable. Unix only -- Windows uses `taskkill /T` for tree termination.
864
+ */
865
+ function killGatewayProcessGroupOrSelf(child, pid, signal) {
866
+ if (pid && pid > 0) {
867
+ try {
868
+ process.kill(-pid, signal);
869
+ return;
870
+ }
871
+ catch {
872
+ // Group signal unavailable (not a group leader / already gone) -- fall back below.
873
+ }
874
+ }
875
+ try {
876
+ child.kill(signal);
877
+ }
878
+ catch {
879
+ // Process already exited.
880
+ }
881
+ }
882
+ /**
883
+ * Stop whatever is listening on the gateway TCP port (covers node …/cli.mjs
884
+ * when argv does not contain the substring "openclaw").
885
+ */
886
+ async function killTcpListenersOnPort(port) {
887
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
888
+ return;
889
+ }
890
+ if (IS_WINDOWS) {
891
+ try {
892
+ const { stdout } = await execAsync(`powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ForEach-Object { $_.OwningProcess } | Sort-Object -Unique"`, { timeout: 10_000 });
893
+ const pids = stdout
894
+ .trim()
895
+ .split(/\r?\n/)
896
+ .map((s) => s.trim())
897
+ .filter((s) => /^\d+$/.test(s));
898
+ for (const pid of pids) {
899
+ await execAsync(`taskkill /F /T /PID ${pid}`, { timeout: 5_000 }).catch(() => { });
900
+ }
901
+ }
902
+ catch {
903
+ // ignore
904
+ }
905
+ return;
906
+ }
907
+ try {
908
+ const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true`, { timeout: 5_000 });
909
+ const pids = [
910
+ ...new Set(stdout
911
+ .trim()
912
+ .split(/\n/)
913
+ .map((s) => s.trim())
914
+ .filter((s) => /^\d+$/.test(s))),
915
+ ];
916
+ for (const pid of pids) {
917
+ await execAsync(`kill ${pid}`, { timeout: 3_000 }).catch(() => { });
918
+ }
919
+ await new Promise((r) => setTimeout(r, 800));
920
+ const { stdout: stdout2 } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null || true`, { timeout: 5_000 });
921
+ const pids2 = [
922
+ ...new Set(stdout2
923
+ .trim()
924
+ .split(/\n/)
925
+ .map((s) => s.trim())
926
+ .filter((s) => /^\d+$/.test(s))),
927
+ ];
928
+ for (const pid of pids2) {
929
+ await execAsync(`kill -9 ${pid}`, { timeout: 3_000 }).catch(() => { });
930
+ }
931
+ }
932
+ catch {
933
+ // lsof missing or failed — ignore
934
+ }
935
+ }
936
+ /**
937
+ * Stop a gateway we did not spawn in-process (detached child, terminal, setup script, etc.).
938
+ * @returns true if the gateway port is no longer accepting connections
939
+ */
940
+ async function stopExternalGatewayListener(port, binaryPath, log) {
941
+ const observedPids = await getListeningProcessIdsForPort(port);
942
+ if (binaryPath) {
943
+ await tryOpenclawGatewayStopCli(binaryPath, log);
944
+ if (await waitForGatewayShutdown(port, observedPids)) {
945
+ return true;
946
+ }
947
+ }
948
+ log(`Stopping listener(s) on port ${port}...`);
949
+ await killTcpListenersOnPort(port);
950
+ const allObservedPids = [...observedPids];
951
+ if (port !== DEFAULT_PORT && (await (0, index_1.probePort)(DEFAULT_PORT))) {
952
+ allObservedPids.push(...(await getListeningProcessIdsForPort(DEFAULT_PORT)));
953
+ log(`Stopping listener(s) on port ${DEFAULT_PORT} (default gateway port)...`);
954
+ await killTcpListenersOnPort(DEFAULT_PORT);
955
+ }
956
+ return waitForGatewayShutdown(port, allObservedPids);
957
+ }
958
+ // Note: findBinary() and probePort() are now imported from ../utils
959
+ // ============================================================================
960
+ // Fast Detection (PID file based, ~1-2ms)
961
+ // Uses shared utilities from ../utils/pid.ts
962
+ // ============================================================================
963
+ /** Wrapper for gateway-specific PID file writes */
964
+ async function writeGatewayPidFile(pid, port, binaryPath) {
965
+ await (0, index_1.writePidFile)(GATEWAY_PID_FILE, pid, port, binaryPath);
966
+ }
967
+ /** Wrapper for gateway-specific PID file removal */
968
+ async function removeGatewayPidFile() {
969
+ await (0, index_1.removePidFile)(GATEWAY_PID_FILE);
970
+ }
971
+ /**
972
+ * Fast gateway detection using PID file.
973
+ * Cost: ~1-2ms when PID file exists and process is running.
974
+ * Falls back to full detection if PID file is missing/stale.
975
+ */
976
+ async function detectGatewayFast(port) {
977
+ const pidData = await (0, index_1.checkPidFileWithCleanup)(GATEWAY_PID_FILE);
978
+ if (!pidData) {
979
+ return null; // No PID file or stale
980
+ }
981
+ // Parse gateway-specific metadata: [port, binaryPath]
982
+ const portFromFile = pidData.metadata?.[0] ? parseInt(pidData.metadata[0], 10) : undefined;
983
+ const binaryPath = pidData.metadata?.[1] || undefined;
984
+ const effectivePort = portFromFile ?? port;
985
+ // PID can be reused by another process; require the gateway port to be listening.
986
+ if (!(await (0, index_1.probePort)(effectivePort))) {
987
+ return null;
988
+ }
989
+ // PID can be stale/reused. Trust fast-path only when listener still looks like our fork.
990
+ if (!(await isTrustedGatewayListener(effectivePort, binaryPath))) {
991
+ return null;
992
+ }
993
+ return {
994
+ installed: true,
995
+ binaryPath,
996
+ running: true,
997
+ port: effectivePort,
998
+ };
999
+ }
1000
+ /**
1001
+ * Get cached binary path, or find it fresh if cache expired.
1002
+ * Uses shared binary finder utility with caching.
1003
+ */
1004
+ async function getCachedBinaryPath(managedRoots) {
1005
+ const roots = managedRoots ?? (await getManagedOpenclawRoots());
1006
+ return getCachedBinaryPathFromRoots(roots);
1007
+ }
1008
+ async function getCachedBinaryPathFromRoots(managedRoots) {
1009
+ const manifestRecord = readManagedGatewayInstallRecord();
1010
+ const manifestRoot = getManagedGatewayPackageRoot(manifestRecord);
1011
+ const manifestBinary = getManagedGatewayBinary(manifestRecord);
1012
+ if (manifestRoot && manifestBinary && fs.existsSync(manifestBinary)) {
1013
+ const manifestMatchesManaged = managedRoots.length === 0 ||
1014
+ managedRoots.some((root) => pathStartsWithPath(manifestRoot, root));
1015
+ if (manifestMatchesManaged) {
1016
+ return manifestBinary;
1017
+ }
1018
+ }
1019
+ const forkCli = await (0, index_1.findBinaryCached)(FORK_CLI_NAME, (0, index_1.getCommonBinaryPaths)(FORK_CLI_NAME));
1020
+ if (forkCli && (await isManagedForkBinaryPath(forkCli, managedRoots))) {
1021
+ return forkCli;
1022
+ }
1023
+ const forkCliFile = IS_WINDOWS ? `${FORK_CLI_NAME}.cmd` : FORK_CLI_NAME;
1024
+ for (const managedRoot of managedRoots) {
1025
+ // Unix: {prefix}/lib/node_modules/openclaw → 3 levels up → {prefix}/bin/cmd
1026
+ // Windows: {prefix}/node_modules/openclaw → 2 levels up → {prefix}/cmd.cmd (no bin/)
1027
+ const prefixDir = IS_WINDOWS
1028
+ ? path.dirname(path.dirname(managedRoot))
1029
+ : path.dirname(path.dirname(path.dirname(managedRoot)));
1030
+ const candidate = IS_WINDOWS
1031
+ ? path.join(prefixDir, forkCliFile)
1032
+ : path.join(prefixDir, "bin", forkCliFile);
1033
+ if (!fs.existsSync(candidate)) {
1034
+ continue;
1035
+ }
1036
+ if (await isManagedForkBinaryPath(candidate, managedRoots)) {
1037
+ return candidate;
1038
+ }
1039
+ }
1040
+ return undefined;
1041
+ }
1042
+ class GatewayLifecycleManager {
1043
+ outputChannel;
1044
+ process = null;
1045
+ healthCheckTimer = null;
1046
+ _status = "not-installed";
1047
+ _bridgeConnected = false;
1048
+ _lastDetection = null;
1049
+ _bridgeRef = null;
1050
+ /** True while `install()` is running setup in a terminal (poll loop). Prevents toggle/detect from resetting UI to "Install" again. */
1051
+ _gatewayInstallInProgress = false;
1052
+ _installProgressMessage;
1053
+ _installSetupTerminal = null;
1054
+ _gatewayUninstallInProgress = false;
1055
+ _uninstallSetupTerminal = null;
1056
+ _startInFlight = null;
1057
+ _lastStartInProgressNoticeAt = 0;
1058
+ /**
1059
+ * Cached answer to "does the gateway have at least one configured messaging channel?" — drives
1060
+ * the connected menu's primary button (Meet at... when channels exist, else Web UI). Undefined
1061
+ * until first probed via {@link refreshHasConfiguredChannels}.
1062
+ */
1063
+ _hasConfiguredChannelsCache;
1064
+ disposables = [];
1065
+ statusBarItem;
1066
+ _onStatusChange = new vscode.EventEmitter();
1067
+ onStatusChange = this._onStatusChange.event;
1068
+ /**
1069
+ * Set the bridge reference (GatewayService) so lifecycle can trigger connect/disconnect.
1070
+ */
1071
+ setBridgeRef(ref) {
1072
+ this._bridgeRef = ref;
1073
+ }
1074
+ constructor(outputChannel, extensionContext) {
1075
+ this.outputChannel = outputChannel;
1076
+ void extensionContext; // Reserved for future use (e.g., storage, secrets)
1077
+ // Status bar at priority 100 (replaces the old GatewayService icon, before Spotlight at 99)
1078
+ this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
1079
+ this.statusBarItem.command = "agentide.anywhere.toggle";
1080
+ this.statusBarItem.name = exports.AGENT_ANYWHERE_LABEL;
1081
+ // Initialize context key for connection state (used by mode picker button)
1082
+ void vscode.commands.executeCommand("setContext", "agentide.anywhere.connected", this._bridgeConnected);
1083
+ this.disposables.push(vscode.window.onDidCloseTerminal((terminal) => {
1084
+ if (this._installSetupTerminal === terminal) {
1085
+ this._installSetupTerminal = null;
1086
+ if (this._gatewayInstallInProgress) {
1087
+ void this.reconcileInstallProgressState();
1088
+ }
1089
+ }
1090
+ if (this._uninstallSetupTerminal === terminal) {
1091
+ this._uninstallSetupTerminal = null;
1092
+ if (this._gatewayUninstallInProgress) {
1093
+ void this.reconcileUninstallProgressState();
1094
+ }
1095
+ }
1096
+ }));
1097
+ this.registerCommands();
1098
+ this.registerConfigWatcher();
1099
+ // Lazy init: only probe if feature is enabled
1100
+ void this.initializeLazy();
1101
+ }
1102
+ setInstallProgressFlag(inProgress, reason) {
1103
+ if (this._gatewayInstallInProgress === inProgress) {
1104
+ return;
1105
+ }
1106
+ this._gatewayInstallInProgress = inProgress;
1107
+ this.log(`Install flag -> ${inProgress ? "true" : `false (${reason})`}`);
1108
+ }
1109
+ get status() {
1110
+ return this._status;
1111
+ }
1112
+ get lastDetection() {
1113
+ return this._lastDetection;
1114
+ }
1115
+ // ============================================================================
1116
+ // Initialization
1117
+ // ============================================================================
1118
+ async initializeLazy() {
1119
+ const hidden = this.getConfig("hidden", false);
1120
+ if (hidden) {
1121
+ this.statusBarItem.hide();
1122
+ return; // Enterprise kill switch: hide everything
1123
+ }
1124
+ const enabled = this.getConfig("enabled", false);
1125
+ if (!enabled) {
1126
+ // Not enabled -- show a subtle status bar hint, no probing
1127
+ this.updateStatusBar("not-installed");
1128
+ this.statusBarItem.show();
1129
+ return;
1130
+ }
1131
+ // Feature enabled -- detect current state (fast path first)
1132
+ const port = this.getGatewayPort();
1133
+ const fast = await detectGatewayFast(port);
1134
+ if (fast) {
1135
+ this._lastDetection = fast;
1136
+ this.setStatus("running");
1137
+ }
1138
+ else {
1139
+ await this.detect();
1140
+ }
1141
+ // Auto-start if configured and installed
1142
+ const autoStart = this.getConfig("autoStart", false);
1143
+ if (autoStart && this._lastDetection?.installed && !this._lastDetection?.running) {
1144
+ this.log("Auto-starting gateway (agentide.anywhere.autoStart = true)");
1145
+ await this.start();
1146
+ }
1147
+ }
1148
+ // ============================================================================
1149
+ // Commands
1150
+ // ============================================================================
1151
+ registerCommands() {
1152
+ this.disposables.push(vscode.commands.registerCommand("agentide.anywhere.toggle", () => this.toggle()), vscode.commands.registerCommand("agentide.anywhere.install", () => this.install()), vscode.commands.registerCommand("agentide.anywhere.uninstall", () => this.uninstall()), vscode.commands.registerCommand("agentide.anywhere.start", () => this.start()), vscode.commands.registerCommand("agentide.anywhere.stop", () => this.stop()), vscode.commands.registerCommand("agentide.anywhere.status", () => this.showStatus()), vscode.commands.registerCommand("agentide.anywhere.openConfig", () => this.openConfig()), vscode.commands.registerCommand("agentide.anywhere.connect", () => this.connectBridge()), vscode.commands.registerCommand("agentide.anywhere.disconnect", () => this.disconnectBridge()), vscode.commands.registerCommand("agentide.anywhere.openWebUI", () => this.openWebUI()), vscode.commands.registerCommand("agentide.anywhere.detect", () => this.detect()),
1153
+ // Exposed for mode picker button: "Meet Agent Anywhere"
1154
+ vscode.commands.registerCommand("agentide.anywhere.meetAgent", () => this.showMeetAgentPicker()));
1155
+ }
1156
+ setBridgeConnected(value) {
1157
+ this._bridgeConnected = value;
1158
+ void vscode.commands.executeCommand("setContext", "agentide.anywhere.connected", value);
1159
+ }
1160
+ registerConfigWatcher() {
1161
+ this.disposables.push(vscode.workspace.onDidChangeConfiguration((e) => {
1162
+ if (e.affectsConfiguration("agentide.anywhere")) {
1163
+ void this.initializeLazy();
1164
+ }
1165
+ }));
1166
+ }
1167
+ // ============================================================================
1168
+ // Toggle (status bar click)
1169
+ // ============================================================================
1170
+ async toggle() {
1171
+ const hidden = this.getConfig("hidden", false);
1172
+ if (hidden) {
1173
+ return;
1174
+ }
1175
+ if (this._gatewayInstallInProgress) {
1176
+ await this.showGatewayInstallInProgressPrompt();
1177
+ return;
1178
+ }
1179
+ if (this._gatewayUninstallInProgress) {
1180
+ await this.showGatewayUninstallInProgressPrompt();
1181
+ return;
1182
+ }
1183
+ if (this._startInFlight) {
1184
+ this.setStatus("starting");
1185
+ const now = Date.now();
1186
+ if (now - this._lastStartInProgressNoticeAt > 4000) {
1187
+ this._lastStartInProgressNoticeAt = now;
1188
+ void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Starting gateway... please wait.`);
1189
+ }
1190
+ return;
1191
+ }
1192
+ // Instant path: route to a menu using cached state only.
1193
+ // Do not block click on detect/start/connect; those happen only after user action.
1194
+ this.setBridgeConnected(this._bridgeRef?.isConnected ?? false);
1195
+ switch (this._status) {
1196
+ case "error": {
1197
+ const likelyInstalled = this._lastDetection?.installed === true;
1198
+ if (likelyInstalled) {
1199
+ await this.showDisconnectedQuickMenu();
1200
+ }
1201
+ else {
1202
+ await this.showNotInstalledQuickMenu();
1203
+ }
1204
+ break;
1205
+ }
1206
+ case "starting":
1207
+ case "stopping":
1208
+ void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Operation in progress...`);
1209
+ break;
1210
+ case "running":
1211
+ if (this._bridgeConnected) {
1212
+ await this.showConnectedQuickMenu();
1213
+ }
1214
+ else {
1215
+ await this.showDisconnectedQuickMenu();
1216
+ }
1217
+ break;
1218
+ case "installed":
1219
+ await this.showDisconnectedQuickMenu();
1220
+ break;
1221
+ case "not-installed":
1222
+ default: {
1223
+ // If cache is stale but we previously saw an installed state, prefer the
1224
+ // disconnected menu and let background refresh correct the UI.
1225
+ const likelyInstalled = this._lastDetection?.installed === true;
1226
+ if (likelyInstalled) {
1227
+ await this.showDisconnectedQuickMenu();
1228
+ break;
1229
+ }
1230
+ await this.showNotInstalledQuickMenu();
1231
+ break;
1232
+ }
1233
+ }
1234
+ }
1235
+ queueBackgroundDetect(reason) {
1236
+ if (this._gatewayInstallInProgress ||
1237
+ this._gatewayUninstallInProgress ||
1238
+ this._startInFlight ||
1239
+ this._status === "starting" ||
1240
+ this._status === "stopping") {
1241
+ return;
1242
+ }
1243
+ setTimeout(() => {
1244
+ if (this._gatewayInstallInProgress ||
1245
+ this._gatewayUninstallInProgress ||
1246
+ this._startInFlight ||
1247
+ this._status === "starting" ||
1248
+ this._status === "stopping") {
1249
+ return;
1250
+ }
1251
+ void this.detect().catch((err) => {
1252
+ this.log(`Background detect failed after ${reason}: ${err instanceof Error ? err.message : String(err)}`);
1253
+ });
1254
+ }, 100);
1255
+ }
1256
+ /**
1257
+ * Level 1 menu for running + connected state.
1258
+ * Connected: Web UI | Disconnect | More | Ask Agent Hippo
1259
+ */
1260
+ async showConnectedQuickMenu() {
1261
+ // Primary action depends on whether messaging channels are configured: surface "Meet at..."
1262
+ // (bind/reclaim a channel for this IDE agent) when they are, otherwise keep "Web UI" since
1263
+ // there is nothing to meet at yet. Cached probe keeps the menu instant; first open awaits once.
1264
+ const hasChannels = this._hasConfiguredChannelsCache ?? (await this.refreshHasConfiguredChannels());
1265
+ const primaryAction = hasChannels ? "Meet at..." : "Web UI";
1266
+ const choicePromise = vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL} is running (bridge: connected).`, primaryAction, "Disconnect", "Ask Agent Hippo", "More");
1267
+ this.queueBackgroundDetect("opening connected menu");
1268
+ // Keep the cache warm for the next open and the More menu's Web UI entry.
1269
+ void this.refreshHasConfiguredChannels();
1270
+ const choice = await choicePromise;
1271
+ switch (choice) {
1272
+ case "Meet at...":
1273
+ await this.showMeetAgentPicker();
1274
+ break;
1275
+ case "Web UI":
1276
+ await this.openWebUI();
1277
+ break;
1278
+ case "Disconnect":
1279
+ await this.disconnectBridge();
1280
+ break;
1281
+ case "More":
1282
+ await this.showMaintenanceQuickMenu();
1283
+ break;
1284
+ case "Ask Agent Hippo":
1285
+ await this.openAskAgentHippoChat();
1286
+ break;
1287
+ }
1288
+ }
1289
+ /**
1290
+ * Level 1 menu for installed but disconnected (or not running) state.
1291
+ * Disconnected: Connect | Reinstall | More | Ask Agent Hippo
1292
+ */
1293
+ async showDisconnectedQuickMenu() {
1294
+ const choicePromise = vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Disconnected.`, "Connect", "Reinstall", "Ask Agent Hippo", "More");
1295
+ this.queueBackgroundDetect("opening disconnected menu");
1296
+ const choice = await choicePromise;
1297
+ switch (choice) {
1298
+ case "Connect":
1299
+ await this.start();
1300
+ if (this._status === "running") {
1301
+ await this.connectBridge();
1302
+ this.setBridgeConnected(this._bridgeRef?.isConnected ?? false);
1303
+ if (this._bridgeConnected) {
1304
+ await this.showConnectedQuickMenu();
1305
+ }
1306
+ }
1307
+ break;
1308
+ case "Reinstall":
1309
+ await this.install();
1310
+ break;
1311
+ case "More":
1312
+ await this.showMaintenanceQuickMenu();
1313
+ break;
1314
+ case "Ask Agent Hippo":
1315
+ await this.openAskAgentHippoChat();
1316
+ break;
1317
+ }
1318
+ }
1319
+ async showGatewayInstallInProgressPrompt() {
1320
+ const choice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Install in progress. Follow the "${exports.AGENT_ANYWHERE_LABEL} Setup" terminal; when it finishes, status updates automatically.`, "Show Setup Terminal", "OK");
1321
+ if (choice === "Show Setup Terminal") {
1322
+ if (!this.revealInstallSetupTerminal()) {
1323
+ await this.showMissingInstallSetupTerminalPrompt();
1324
+ }
1325
+ }
1326
+ }
1327
+ async showMissingInstallSetupTerminalPrompt() {
1328
+ const result = await this.reconcileInstallProgressState();
1329
+ if (result?.installed) {
1330
+ void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Install completed. Status has been refreshed.`);
1331
+ }
1332
+ else {
1333
+ void vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Setup terminal is no longer available. Install did not complete; run Install again to relaunch setup.`);
1334
+ }
1335
+ }
1336
+ async showGatewayUninstallInProgressPrompt() {
1337
+ const choice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Uninstall in progress. Follow the "${AGENT_ANYWHERE_UNINSTALL_TERMINAL}" terminal; when it finishes, status updates automatically.`, "Show Uninstall Terminal", "OK");
1338
+ if (choice === "Show Uninstall Terminal") {
1339
+ if (!this.revealUninstallSetupTerminal()) {
1340
+ await this.showMissingUninstallSetupTerminalPrompt();
1341
+ }
1342
+ }
1343
+ }
1344
+ async showMissingUninstallSetupTerminalPrompt() {
1345
+ const result = await this.reconcileUninstallProgressState();
1346
+ if (result && !result.installed && !result.running) {
1347
+ void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Uninstall completed. Status has been refreshed.`);
1348
+ }
1349
+ else {
1350
+ void vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Uninstall terminal is no longer available. Uninstall may still be running or did not complete; run Status to confirm.`);
1351
+ }
1352
+ }
1353
+ revealInstallSetupTerminal() {
1354
+ const terminal = this._installSetupTerminal ??
1355
+ vscode.window.terminals.find((t) => t.name === AGENT_ANYWHERE_SETUP_TERMINAL ||
1356
+ t.name.startsWith(`${AGENT_ANYWHERE_SETUP_TERMINAL} `)) ??
1357
+ null;
1358
+ if (!terminal) {
1359
+ return false;
1360
+ }
1361
+ this._installSetupTerminal = terminal;
1362
+ terminal.show();
1363
+ return true;
1364
+ }
1365
+ revealUninstallSetupTerminal() {
1366
+ const terminal = this._uninstallSetupTerminal ??
1367
+ vscode.window.terminals.find((t) => t.name === AGENT_ANYWHERE_UNINSTALL_TERMINAL ||
1368
+ t.name.startsWith(`${AGENT_ANYWHERE_UNINSTALL_TERMINAL} `)) ??
1369
+ null;
1370
+ if (!terminal) {
1371
+ return false;
1372
+ }
1373
+ this._uninstallSetupTerminal = terminal;
1374
+ terminal.show();
1375
+ return true;
1376
+ }
1377
+ async reconcileInstallProgressState() {
1378
+ if (!this._gatewayInstallInProgress) {
1379
+ return this._lastDetection;
1380
+ }
1381
+ if (this.revealInstallSetupTerminal()) {
1382
+ return this._lastDetection;
1383
+ }
1384
+ // Setup terminal closed — allow npm/manifest to settle before treating as failure.
1385
+ await this.sleep(2_000);
1386
+ if (this.revealInstallSetupTerminal()) {
1387
+ return this._lastDetection;
1388
+ }
1389
+ (0, index_1.clearBinaryCache)(FORK_CLI_NAME);
1390
+ (0, index_1.clearBinaryCache)("openclaw");
1391
+ const result = await this.detectGatewayState(this.getGatewayPort());
1392
+ this._lastDetection = result;
1393
+ if (result.running) {
1394
+ this.finishInstallUiState("running", "terminal reconciliation");
1395
+ }
1396
+ else if (result.installed) {
1397
+ this.finishInstallUiState("installed", "terminal reconciliation");
1398
+ }
1399
+ else {
1400
+ this.log("Setup terminal closed before gateway install was detected");
1401
+ this.setInstallProgressFlag(false, "terminal reconciliation");
1402
+ this._installProgressMessage = undefined;
1403
+ this.setStatus("not-installed");
1404
+ }
1405
+ return result;
1406
+ }
1407
+ finishInstallUiState(status, reason) {
1408
+ this.setInstallProgressFlag(false, reason);
1409
+ this._installProgressMessage = undefined;
1410
+ this.setStatus(status);
1411
+ }
1412
+ readInstallStatusSnapshot() {
1413
+ return readGatewayInstallStatusSnapshot();
1414
+ }
1415
+ getInstallProgressSummary(status) {
1416
+ return getGatewayInstallProgressSummary(status);
1417
+ }
1418
+ async detectGatewayState(port) {
1419
+ return detectGateway(port);
1420
+ }
1421
+ async detectGatewayFastState(port) {
1422
+ return detectGatewayFast(port);
1423
+ }
1424
+ async probeGatewayPort(port) {
1425
+ return (0, index_1.probePort)(port);
1426
+ }
1427
+ isStartedGatewayProcessAlive(pid) {
1428
+ return pid !== undefined && (0, index_1.isProcessRunning)(pid);
1429
+ }
1430
+ getStartupTimeoutMs() {
1431
+ return STARTUP_TIMEOUT_MS;
1432
+ }
1433
+ getStartupCheckIntervalMs() {
1434
+ return 500;
1435
+ }
1436
+ getStartupLaunchGraceMs() {
1437
+ return IS_WINDOWS ? 15_000 : 0;
1438
+ }
1439
+ getUninstallInitialDelayMs() {
1440
+ return IS_WINDOWS ? 2_000 : 3_000;
1441
+ }
1442
+ getUninstallVerificationTimeoutMs() {
1443
+ return IS_WINDOWS ? 15_000 : 90_000;
1444
+ }
1445
+ getUninstallCheckIntervalMs() {
1446
+ return IS_WINDOWS ? 1_000 : 4_000;
1447
+ }
1448
+ async sleep(ms) {
1449
+ await new Promise((resolve) => setTimeout(resolve, ms));
1450
+ }
1451
+ /**
1452
+ * Whether the gateway launch should take the Windows (hidden-process) path. Defaults to the
1453
+ * platform constant; exposed as a method so tests can force the Windows branch off-Windows.
1454
+ */
1455
+ isWindowsLaunch() {
1456
+ return IS_WINDOWS;
1457
+ }
1458
+ async stopExistingGatewayBeforeStart(binaryPath) {
1459
+ await tryOpenclawGatewayStopCli(binaryPath, (message) => this.log(message));
1460
+ }
1461
+ getGatewayLockPathForConfig() {
1462
+ const configHash = (0, crypto_1.createHash)("sha256").update(this.configPath).digest("hex").slice(0, 8);
1463
+ return path.join(os.tmpdir(), "openclaw", `gateway.${configHash}.lock`);
1464
+ }
1465
+ async removeStaleGatewayLockBeforeStart(port) {
1466
+ if (!IS_WINDOWS) {
1467
+ return;
1468
+ }
1469
+ const lockPath = this.getGatewayLockPathForConfig();
1470
+ if (!fs.existsSync(lockPath)) {
1471
+ return;
1472
+ }
1473
+ if (await this.probeGatewayPort(port)) {
1474
+ this.log(`Gateway lock present but port ${port} is open; leaving lock in place.`);
1475
+ return;
1476
+ }
1477
+ let lockAgeMs = Number.POSITIVE_INFINITY;
1478
+ try {
1479
+ const st = await fs.promises.stat(lockPath);
1480
+ lockAgeMs = Date.now() - st.mtimeMs;
1481
+ }
1482
+ catch (error) {
1483
+ this.log(`Could not stat gateway lock before start: ${error instanceof Error ? error.message : String(error)}`);
1484
+ return;
1485
+ }
1486
+ if (lockAgeMs < STALE_GATEWAY_LOCK_MIN_AGE_MS) {
1487
+ this.log(`Gateway lock is only ${Math.round(lockAgeMs / 1000)}s old; not removing (possible concurrent start still binding).`);
1488
+ return;
1489
+ }
1490
+ try {
1491
+ fs.rmSync(lockPath, { force: true });
1492
+ this.log(`Removed stale gateway lock before start (${Math.round(lockAgeMs / 1000)}s old): ${lockPath}`);
1493
+ }
1494
+ catch (error) {
1495
+ this.log(`Could not remove stale gateway lock before start: ${error instanceof Error ? error.message : String(error)}`);
1496
+ }
1497
+ }
1498
+ async writeGatewayPidRecord(pid, port, binaryPath) {
1499
+ await writeGatewayPidFile(pid, port, binaryPath);
1500
+ }
1501
+ async reconcileUninstallProgressState() {
1502
+ if (!this._gatewayUninstallInProgress) {
1503
+ return this._lastDetection;
1504
+ }
1505
+ if (this.revealUninstallSetupTerminal()) {
1506
+ return this._lastDetection;
1507
+ }
1508
+ const result = await detectGateway(this.getGatewayPort());
1509
+ this._lastDetection = result;
1510
+ this._gatewayUninstallInProgress = false;
1511
+ if (result.running) {
1512
+ this.setStatus("running");
1513
+ }
1514
+ else if (result.installed) {
1515
+ this.setStatus("installed");
1516
+ }
1517
+ else {
1518
+ this.setStatus("not-installed");
1519
+ }
1520
+ return result;
1521
+ }
1522
+ /**
1523
+ * Level 1 menu for not-installed state.
1524
+ * Not installed: Install | Ask Agent Hippo
1525
+ */
1526
+ async showNotInstalledQuickMenu() {
1527
+ const choicePromise = vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Reach your AgentHippo IDE or CLI agents from WhatsApp, Telegram, Slack, Discord, and more - right from your phone or any messaging app. This will also install OpenClaw fork on your computer.`, "Install", "Ask Agent Hippo");
1528
+ this.queueBackgroundDetect("opening not-installed menu");
1529
+ const choice = await choicePromise;
1530
+ switch (choice) {
1531
+ case "Install":
1532
+ await this.install();
1533
+ break;
1534
+ case "Ask Agent Hippo":
1535
+ await this.openAskAgentHippoChat();
1536
+ break;
1537
+ }
1538
+ }
1539
+ /**
1540
+ * Level 2 maintenance menu.
1541
+ * Config | Reset auth | Restart (only if OpenClaw appears installed) | Uninstall
1542
+ */
1543
+ async showMaintenanceQuickMenu() {
1544
+ // Keep "More" instant: build actions from cached in-memory state and
1545
+ // refresh detection in the background.
1546
+ const likelyInstalled = this._status === "running" ||
1547
+ this._status === "installed" ||
1548
+ this._lastDetection?.installed === true;
1549
+ // Web UI lives here once channels are configured (the connected menu's front button becomes
1550
+ // "Meet at..." in that case); keep it reachable but out of the way.
1551
+ const showWebUi = this._status === "running" && this._hasConfiguredChannelsCache === true;
1552
+ const actions = [
1553
+ ...(showWebUi ? ["Web UI"] : []),
1554
+ "Config",
1555
+ "Reset auth",
1556
+ "Reset mappings",
1557
+ ...(likelyInstalled ? ["Restart"] : []),
1558
+ "Uninstall",
1559
+ ];
1560
+ const choicePromise = vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: More options`, ...actions);
1561
+ this.queueBackgroundDetect("opening maintenance menu");
1562
+ const choice = await choicePromise;
1563
+ switch (choice) {
1564
+ case "Web UI":
1565
+ await this.openWebUI();
1566
+ break;
1567
+ case "Config":
1568
+ await this.openConfig();
1569
+ break;
1570
+ case "Reset auth":
1571
+ await this.resetAuth();
1572
+ break;
1573
+ case "Reset mappings":
1574
+ await vscode.commands.executeCommand("agentide.gateway.resetMappings");
1575
+ break;
1576
+ case "Restart":
1577
+ await this.restartGateway();
1578
+ break;
1579
+ case "Uninstall":
1580
+ await this.uninstall();
1581
+ break;
1582
+ }
1583
+ }
1584
+ /**
1585
+ * Best-effort `channels.status` probe used to choose the connected menu's primary button. Caches
1586
+ * and returns whether any messaging channel is configured. On an unreachable gateway it returns
1587
+ * the last-known value (or false), so the menu falls back to "Web UI" rather than hanging.
1588
+ */
1589
+ async refreshHasConfiguredChannels() {
1590
+ const ref = this._bridgeRef;
1591
+ if (!ref?.request) {
1592
+ return this._hasConfiguredChannelsCache ?? false;
1593
+ }
1594
+ try {
1595
+ const { rows } = await (0, openclaw_channel_routing_js_1.fetchMeetAgentChannelContext)(ref.request.bind(ref));
1596
+ this._hasConfiguredChannelsCache = rows.length > 0;
1597
+ }
1598
+ catch (err) {
1599
+ this.log(`channels.status probe failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
1600
+ this._hasConfiguredChannelsCache ??= false;
1601
+ }
1602
+ return this._hasConfiguredChannelsCache;
1603
+ }
1604
+ async openAskAgentHippoChat() {
1605
+ await vscode.commands.executeCommand("workbench.action.chat.newChat", {
1606
+ agentMode: true,
1607
+ });
1608
+ const initialQuery = "use openclaw skill as needed.";
1609
+ await vscode.commands.executeCommand("workbench.action.chat.open", {
1610
+ mode: "agent",
1611
+ query: initialQuery,
1612
+ // Prefill without auto-sending; user can review/edit before sending.
1613
+ isPartialQuery: true,
1614
+ });
1615
+ }
1616
+ // ============================================================================
1617
+ // Meet Agent at... (channel binding picker)
1618
+ // ============================================================================
1619
+ /**
1620
+ * Show a QuickPick to choose which channels this agent should receive messages from.
1621
+ * Queries the gateway for available channels and current bindings, then writes
1622
+ * the selected bindings back via config.patch (route bindings).
1623
+ */
1624
+ async showMeetAgentPicker(agentId) {
1625
+ if (!this._bridgeRef?.request) {
1626
+ vscode.window.showWarningMessage("Not connected to gateway.");
1627
+ return;
1628
+ }
1629
+ const request = this._bridgeRef.request.bind(this._bridgeRef);
1630
+ let didScopeRetry = false;
1631
+ const requestWithScopeRetry = async (method, params) => {
1632
+ try {
1633
+ return await request(method, params);
1634
+ }
1635
+ catch (err) {
1636
+ const message = err instanceof Error ? err.message : String(err);
1637
+ if (!didScopeRetry && (0, gateway_scope_errors_js_1.isGatewayScopeAuthError)(message)) {
1638
+ didScopeRetry = true;
1639
+ this.log(`Gateway denied RPC (${message}); reconnecting with fresh handshake...`);
1640
+ await this.disconnectBridge();
1641
+ await this.connectBridge();
1642
+ // Rebind after reconnect just in case the ref changed.
1643
+ if (!this._bridgeRef?.request) {
1644
+ throw err;
1645
+ }
1646
+ return await this._bridgeRef.request(method, params);
1647
+ }
1648
+ throw err;
1649
+ }
1650
+ };
1651
+ try {
1652
+ // Fetch available channels from the gateway (OpenClaw: channels.status).
1653
+ this.log("Meet Agent: fetching channels.status...");
1654
+ const { rows: channels, statusPayload } = await (0, openclaw_channel_routing_js_1.fetchMeetAgentChannelContext)(requestWithScopeRetry);
1655
+ this.log(`Meet Agent: got ${channels?.length ?? 0} channel(s): ${channels?.map((c) => c.id).join(", ") ?? "none"}`);
1656
+ if (!channels || channels.length === 0) {
1657
+ const choice = await vscode.window.showInformationMessage("No messaging channels (Telegram, WhatsApp, etc.) are configured. You can chat with your agent in the Gateway Web UI, or set up channels in OpenClaw config.", "Open Web UI", "Open Config");
1658
+ if (choice === "Open Web UI") {
1659
+ await this.openWebUI();
1660
+ }
1661
+ else if (choice === "Open Config") {
1662
+ await this.openConfig();
1663
+ }
1664
+ return;
1665
+ }
1666
+ // Fetch current bindings for this agent to pre-select.
1667
+ // Use the first registered agent if no specific agentId provided.
1668
+ const resolvedAgentId = agentId ?? await this.getFirstRegisteredAgentId();
1669
+ if (!resolvedAgentId) {
1670
+ vscode.window.showWarningMessage("No agent registered with the gateway.");
1671
+ return;
1672
+ }
1673
+ // Get current bindings from the agents.register response or agents.list.
1674
+ const agentsList = await requestWithScopeRetry("agents.list");
1675
+ const externalAgent = agentsList.external?.find((a) => a.id.toLowerCase() === resolvedAgentId.toLowerCase());
1676
+ const currentlyBound = new Set(externalAgent?.boundChannels?.map((c) => c.toLowerCase()) ?? []);
1677
+ const items = channels.map((ch) => ({
1678
+ channelId: ch.id,
1679
+ label: ch.label,
1680
+ description: ch.connected ? "connected" : "offline",
1681
+ detail: ch.accounts.length > 1 ? `Accounts: ${ch.accounts.join(", ")}` : undefined,
1682
+ picked: currentlyBound.has(ch.id.toLowerCase()),
1683
+ }));
1684
+ const picked = await vscode.window.showQuickPick(items, {
1685
+ canPickMany: true,
1686
+ title: `Meet ${resolvedAgentId} at...`,
1687
+ placeHolder: "Select channels where people can reach this agent",
1688
+ });
1689
+ if (!picked) {
1690
+ return; // User cancelled.
1691
+ }
1692
+ const selectedChannels = picked.map((item) => item.channelId);
1693
+ // Write route bindings via config.patch (replaces removed bindings.set).
1694
+ await (0, openclaw_channel_routing_js_1.applyAgentChannelBindingsViaConfigPatch)(requestWithScopeRetry, {
1695
+ agentId: resolvedAgentId,
1696
+ selectedChannelIds: selectedChannels,
1697
+ statusPayload,
1698
+ });
1699
+ if (selectedChannels.length === 0) {
1700
+ vscode.window.showInformationMessage(`${resolvedAgentId}: all channel bindings removed.`);
1701
+ }
1702
+ else {
1703
+ const names = picked.map((item) => item.label).join(", ");
1704
+ vscode.window.showInformationMessage(`${resolvedAgentId} can now be reached at: ${names}`);
1705
+ }
1706
+ }
1707
+ catch (err) {
1708
+ this.log(`Meet Agent picker failed: ${err}`);
1709
+ vscode.window.showErrorMessage(`Failed to configure channels: ${err instanceof Error ? err.message : String(err)}`);
1710
+ }
1711
+ }
1712
+ async getFirstRegisteredAgentId() {
1713
+ if (!this._bridgeRef?.request) {
1714
+ return undefined;
1715
+ }
1716
+ try {
1717
+ const result = await this._bridgeRef.request("agents.list");
1718
+ return result.external?.[0]?.id;
1719
+ }
1720
+ catch {
1721
+ return undefined;
1722
+ }
1723
+ }
1724
+ // ============================================================================
1725
+ // Detect
1726
+ // ============================================================================
1727
+ async detect() {
1728
+ const port = this.getGatewayPort();
1729
+ const baseResult = await detectGateway(port);
1730
+ let result = baseResult;
1731
+ // Do not clobber in-progress lifecycle UI while setup/teardown runs in terminals.
1732
+ if (this._gatewayInstallInProgress || this._gatewayUninstallInProgress) {
1733
+ this._lastDetection = result;
1734
+ return result;
1735
+ }
1736
+ // Do not downgrade "starting" back to "installed" while start() is in-flight.
1737
+ if (this._startInFlight && this._status === "starting") {
1738
+ this._lastDetection = result;
1739
+ return result;
1740
+ }
1741
+ if (!result.running && result.installed) {
1742
+ const managedInstallPresent = await this.hasGlobalNpmOpenclawTree();
1743
+ if (!managedInstallPresent) {
1744
+ result = { ...result, installed: false };
1745
+ }
1746
+ }
1747
+ this._lastDetection = result;
1748
+ if (result.running) {
1749
+ this.setStatus("running");
1750
+ }
1751
+ else if (result.installed) {
1752
+ this.setStatus("installed");
1753
+ }
1754
+ else {
1755
+ this.setStatus("not-installed");
1756
+ }
1757
+ return result;
1758
+ }
1759
+ // ============================================================================
1760
+ // Install
1761
+ // ============================================================================
1762
+ async install() {
1763
+ const installSource = this.getConfig("installSource", "");
1764
+ const port = this.getGatewayPort();
1765
+ if (!installSource) {
1766
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Install source not configured. Set "agentide.anywhere.installSource" in settings.`);
1767
+ return false;
1768
+ }
1769
+ // Security: validate installSource to prevent command injection.
1770
+ // Allowed: npm package names, scoped packages, git repos, local paths, version specifiers.
1771
+ // Reject: semicolons, backticks, pipes, $(), &&, ||, newlines, etc.
1772
+ if (/[;`|$&\n\r]/.test(installSource) || /\$\(/.test(installSource)) {
1773
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Invalid install source - contains disallowed characters. Check "agentide.anywhere.installSource" in settings.`);
1774
+ this.log(`Rejected installSource due to suspicious characters: ${installSource}`);
1775
+ return false;
1776
+ }
1777
+ if (this._gatewayInstallInProgress) {
1778
+ const dupChoice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Install is already in progress. Watch the "${exports.AGENT_ANYWHERE_LABEL} Setup" terminal.`, "Show Setup Terminal", "OK");
1779
+ if (dupChoice === "Show Setup Terminal") {
1780
+ if (!this.revealInstallSetupTerminal()) {
1781
+ await this.showMissingInstallSetupTerminalPrompt();
1782
+ }
1783
+ }
1784
+ return false;
1785
+ }
1786
+ this.setInstallProgressFlag(true, "install command started");
1787
+ try {
1788
+ this.log(`Installing gateway from: ${installSource}`);
1789
+ (0, index_1.clearBinaryCache)(FORK_CLI_NAME);
1790
+ (0, index_1.clearBinaryCache)("openclaw");
1791
+ // Prevent the status bar from flipping to "running" while install is in
1792
+ // progress. During the install window, the port may transiently open,
1793
+ // and `detect()` would otherwise turn the icon green early.
1794
+ this.setStatus("starting");
1795
+ this.stopHealthChecks();
1796
+ // Stop a running gateway before setup so npm/git can replace the global
1797
+ // CLI without file locks on the binary (especially on Windows).
1798
+ const snapshot = await this.detectGatewayState(port);
1799
+ this._lastDetection = snapshot;
1800
+ if (snapshot.running) {
1801
+ this.log("Stopping gateway before install...");
1802
+ await this.disconnectBridge();
1803
+ await this.stop();
1804
+ }
1805
+ // Find the setup script (platform-specific: .ps1 on Windows, .sh elsewhere)
1806
+ const scriptExt = IS_WINDOWS ? "setup-gateway.ps1" : "setup-gateway.sh";
1807
+ const scriptCandidates = [
1808
+ path.join(__dirname, "..", "..", "scripts", "gateway", scriptExt),
1809
+ path.join(__dirname, "..", "scripts", "gateway", scriptExt),
1810
+ ];
1811
+ const scriptPath = scriptCandidates.find((p) => fs.existsSync(p));
1812
+ // Find the config template
1813
+ const templateCandidates = [
1814
+ path.join(__dirname, "..", "..", "base-prompts", "gateway", "openclaw-starter.json5"),
1815
+ path.join(__dirname, "..", "base-prompts", "gateway", "openclaw-starter.json5"),
1816
+ ];
1817
+ const templatePath = templateCandidates.find((p) => fs.existsSync(p));
1818
+ let terminalOptions;
1819
+ if (scriptPath && IS_WINDOWS) {
1820
+ // Windows: run PowerShell script
1821
+ const args = [`-Source "${installSource}"`];
1822
+ if (templatePath) {
1823
+ args.push(`-ConfigTemplate "${templatePath}"`);
1824
+ }
1825
+ const psCmd = `& "${scriptPath}" ${args.join(" ")}; Write-Host ""; Write-Host "You can close this window."`;
1826
+ terminalOptions = {
1827
+ name: AGENT_ANYWHERE_SETUP_TERMINAL,
1828
+ shellPath: "powershell.exe",
1829
+ shellArgs: ["-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-Command", psCmd],
1830
+ };
1831
+ }
1832
+ else if (scriptPath) {
1833
+ // macOS/Linux: run bash script
1834
+ const args = [`--source "${installSource}"`];
1835
+ if (templatePath) {
1836
+ args.push(`--config-template "${templatePath}"`);
1837
+ }
1838
+ const setupCmd = `bash "${scriptPath}" ${args.join(" ")}`;
1839
+ const holdOpenCmd = `${setupCmd}; echo ""; echo "Press Enter to close..."; read`;
1840
+ terminalOptions = {
1841
+ name: AGENT_ANYWHERE_SETUP_TERMINAL,
1842
+ shellPath: getAnywhereSetupShellPath(),
1843
+ shellArgs: getAnywhereSetupShellArgs(holdOpenCmd),
1844
+ env: anywhereSetupTerminalEnv(),
1845
+ };
1846
+ }
1847
+ else {
1848
+ // Fallback: raw npm install (script not bundled)
1849
+ this.log("Setup script not found, falling back to npm install");
1850
+ const fallbackCmd = `npm install -g ${installSource}`;
1851
+ const holdOpenCmd = `${fallbackCmd}; echo ""; echo "Press Enter to close..."; read`;
1852
+ terminalOptions = IS_WINDOWS
1853
+ ? { name: AGENT_ANYWHERE_SETUP_TERMINAL, shellPath: "cmd.exe", shellArgs: ["/c", `${fallbackCmd} && echo. && echo Setup complete. && pause`] }
1854
+ : {
1855
+ name: AGENT_ANYWHERE_SETUP_TERMINAL,
1856
+ shellPath: getAnywhereSetupShellPath(),
1857
+ shellArgs: getAnywhereSetupShellArgs(holdOpenCmd),
1858
+ env: anywhereSetupTerminalEnv(),
1859
+ };
1860
+ }
1861
+ // Run in a visible terminal so user can see progress
1862
+ try {
1863
+ fs.rmSync(GATEWAY_INSTALL_STATUS_FILE, { force: true });
1864
+ }
1865
+ catch {
1866
+ // ignore stale install status cleanup failures
1867
+ }
1868
+ this._installProgressMessage = "Launching setup...";
1869
+ this.updateStatusBar("starting");
1870
+ const terminal = vscode.window.createTerminal(terminalOptions);
1871
+ this._installSetupTerminal = terminal;
1872
+ terminal.show();
1873
+ const installTimeoutMs = installSourceNeedsBuild(installSource)
1874
+ ? 10 * 60_000
1875
+ : 120_000;
1876
+ this.log(`Waiting up to ${Math.round(installTimeoutMs / 60_000)} minute(s) for gateway install completion...`);
1877
+ const useInstallStatusMarker = IS_WINDOWS && !!scriptPath;
1878
+ const installSucceeded = await vscode.window.withProgress({
1879
+ location: vscode.ProgressLocation.Window,
1880
+ title: `${exports.AGENT_ANYWHERE_LABEL}: Installing gateway...`,
1881
+ cancellable: false,
1882
+ }, async (progress) => {
1883
+ const deadline = Date.now() + installTimeoutMs;
1884
+ let lastPercent = 0;
1885
+ while (Date.now() < deadline) {
1886
+ if (!this._gatewayInstallInProgress) {
1887
+ this.log("Gateway install wait aborted — setup terminal closed before completion was detected");
1888
+ return false;
1889
+ }
1890
+ const installStatus = this.readInstallStatusSnapshot();
1891
+ const summary = this.getInstallProgressSummary(installStatus);
1892
+ this._installProgressMessage = summary.message;
1893
+ this.updateStatusBar("starting");
1894
+ const increment = Math.max(0, summary.percent - lastPercent);
1895
+ progress.report({ message: summary.message, increment });
1896
+ lastPercent = Math.max(lastPercent, summary.percent);
1897
+ if (installStatus?.phase === "failed") {
1898
+ this.log(`Gateway install failed: ${installStatus.lastError || installStatus.message || "unknown error"}`);
1899
+ return false;
1900
+ }
1901
+ (0, index_1.clearBinaryCache)(FORK_CLI_NAME);
1902
+ (0, index_1.clearBinaryCache)("openclaw");
1903
+ const result = await this.detectGatewayState(port);
1904
+ this._lastDetection = result;
1905
+ const installReady = useInstallStatusMarker
1906
+ ? isGatewayInstallComplete(installStatus) && result.installed
1907
+ : result.installed;
1908
+ if (installReady) {
1909
+ this.log("Gateway installed successfully");
1910
+ await this.ensureManagedGatewayWrapperState(result.binaryPath);
1911
+ // Build the web Control UI if it's missing.
1912
+ // The setup script should do this, but if it failed or was
1913
+ // skipped (e.g. missing pnpm), build it now so the UI works
1914
+ // out of the box for new users.
1915
+ await this.ensureWebUIBuilt(result.binaryPath);
1916
+ // Write starter config if none exists
1917
+ this.writeStarterConfig();
1918
+ // Auto-enable the feature now that it's installed
1919
+ const anywhereConfig = vscode.workspace.getConfiguration("agentide.anywhere");
1920
+ await anywhereConfig.update("enabled", true, vscode.ConfigurationTarget.Global);
1921
+ if (result.running) {
1922
+ this.finishInstallUiState("running", "success");
1923
+ progress.report({
1924
+ message: `Install complete. Gateway is running on port ${port}.`,
1925
+ increment: Math.max(0, 100 - lastPercent),
1926
+ });
1927
+ }
1928
+ else {
1929
+ this.finishInstallUiState("installed", "success");
1930
+ progress.report({
1931
+ message: "Install complete. Gateway is installed. Click Agent Anywhere to start.",
1932
+ increment: Math.max(0, 100 - lastPercent),
1933
+ });
1934
+ }
1935
+ // Install no longer auto-starts the gateway; starting is an explicit user action.
1936
+ void vscode.window.showInformationMessage(result.running
1937
+ ? `${exports.AGENT_ANYWHERE_LABEL}: Installed and running. Open the config to add your channels or use the status bar to connect.`
1938
+ : `${exports.AGENT_ANYWHERE_LABEL}: Installed. Open the config to add your channels, then click Agent Anywhere to start.`, "Open Config").then(async (choice) => {
1939
+ if (choice === "Open Config") {
1940
+ await this.openConfig();
1941
+ }
1942
+ });
1943
+ return true;
1944
+ }
1945
+ await new Promise((resolve) => setTimeout(resolve, 1_000));
1946
+ }
1947
+ return false;
1948
+ });
1949
+ if (!installSucceeded) {
1950
+ this.log("Gateway install timed out or failed");
1951
+ this.finishInstallUiState("error", "failure");
1952
+ return false;
1953
+ }
1954
+ return true;
1955
+ }
1956
+ catch (e) {
1957
+ this.log(`Install failed: ${e instanceof Error ? e.message : String(e)}`);
1958
+ this.finishInstallUiState("error", "failure");
1959
+ return false;
1960
+ }
1961
+ finally {
1962
+ if (this._gatewayInstallInProgress) {
1963
+ this.setInstallProgressFlag(false, "finally");
1964
+ this._installProgressMessage = undefined;
1965
+ }
1966
+ this.updateStatusBar(this._status);
1967
+ }
1968
+ }
1969
+ /**
1970
+ * Remove the globally installed `openclaw` npm package (runs setup script --uninstall / -Uninstall).
1971
+ * The script runs `openclaw uninstall --service --yes --non-interactive` when the CLI exists,
1972
+ * then `npm uninstall -g openclaw`, then frees loopback listener(s) if still bound (see OpenClaw docs: /cli/uninstall).
1973
+ * Does not delete ~/.openclaw (config, sessions, credentials) unless the user runs OpenClaw with --state.
1974
+ */
1975
+ async uninstall() {
1976
+ if (this._gatewayUninstallInProgress) {
1977
+ await this.showGatewayUninstallInProgressPrompt();
1978
+ return;
1979
+ }
1980
+ await this.stop();
1981
+ const scriptExt = IS_WINDOWS ? "setup-gateway.ps1" : "setup-gateway.sh";
1982
+ const scriptCandidates = [
1983
+ path.join(__dirname, "..", "scripts", "gateway", scriptExt),
1984
+ path.join(__dirname, "..", "..", "scripts", "gateway", scriptExt),
1985
+ ];
1986
+ const scriptPath = scriptCandidates.find((p) => fs.existsSync(p));
1987
+ let terminalOptions;
1988
+ if (scriptPath && IS_WINDOWS) {
1989
+ const psCmd = `& "${scriptPath}" -Uninstall; Write-Host ""; Write-Host "You can close this window."`;
1990
+ terminalOptions = {
1991
+ name: AGENT_ANYWHERE_UNINSTALL_TERMINAL,
1992
+ shellPath: "powershell.exe",
1993
+ shellArgs: ["-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-Command", psCmd],
1994
+ };
1995
+ }
1996
+ else if (scriptPath) {
1997
+ const setupCmd = `bash "${scriptPath}" --uninstall`;
1998
+ const holdOpenCmd = `${setupCmd}; echo ""; echo "Press Enter to close..."; read`;
1999
+ terminalOptions = {
2000
+ name: AGENT_ANYWHERE_UNINSTALL_TERMINAL,
2001
+ shellPath: getAnywhereSetupShellPath(),
2002
+ shellArgs: getAnywhereSetupShellArgs(holdOpenCmd),
2003
+ env: anywhereSetupTerminalEnv(),
2004
+ };
2005
+ }
2006
+ else {
2007
+ const fallbackCmd = "npm uninstall -g openclaw";
2008
+ const holdOpenCmd = `${fallbackCmd}; echo ""; echo "Press Enter to close..."; read`;
2009
+ terminalOptions = IS_WINDOWS
2010
+ ? {
2011
+ name: AGENT_ANYWHERE_UNINSTALL_TERMINAL,
2012
+ shellPath: "cmd.exe",
2013
+ shellArgs: ["/c", `${fallbackCmd} && echo. && pause`],
2014
+ }
2015
+ : {
2016
+ name: AGENT_ANYWHERE_UNINSTALL_TERMINAL,
2017
+ shellPath: getAnywhereSetupShellPath(),
2018
+ shellArgs: getAnywhereSetupShellArgs(holdOpenCmd),
2019
+ env: anywhereSetupTerminalEnv(),
2020
+ };
2021
+ }
2022
+ this._gatewayUninstallInProgress = true;
2023
+ this.setStatus("stopping");
2024
+ try {
2025
+ const terminal = vscode.window.createTerminal(terminalOptions);
2026
+ this._uninstallSetupTerminal = terminal;
2027
+ terminal.show();
2028
+ this.log("Gateway uninstall started in terminal");
2029
+ (0, index_1.clearBinaryCache)(FORK_CLI_NAME);
2030
+ (0, index_1.clearBinaryCache)("openclaw");
2031
+ await this.sleep(this.getUninstallInitialDelayMs());
2032
+ const deadline = Date.now() + this.getUninstallVerificationTimeoutMs();
2033
+ let lastResult;
2034
+ let managedInstallPresent = true;
2035
+ while (Date.now() < deadline) {
2036
+ (0, index_1.clearBinaryCache)(FORK_CLI_NAME);
2037
+ (0, index_1.clearBinaryCache)("openclaw");
2038
+ const result = await this.detectGatewayState(this.getGatewayPort());
2039
+ lastResult = result;
2040
+ this._lastDetection = result;
2041
+ managedInstallPresent = await this.hasGlobalNpmOpenclawTree();
2042
+ // If the npm-managed OpenClaw package is gone and no local listener is alive,
2043
+ // consider Agent Anywhere uninstall complete even if another `openclaw`
2044
+ // binary remains on PATH (for example, a self-updated/external install).
2045
+ if (!managedInstallPresent && !result.running) {
2046
+ removeManagedGatewayWrapperState();
2047
+ this.setStatus("not-installed");
2048
+ if (result.installed) {
2049
+ this.log("Managed OpenClaw npm package removed; another OpenClaw binary remains on PATH (likely external/self-updated).");
2050
+ }
2051
+ else {
2052
+ this.log("Global OpenClaw package removed (or no longer detected).");
2053
+ }
2054
+ return;
2055
+ }
2056
+ if (!result.installed) {
2057
+ removeManagedGatewayWrapperState();
2058
+ this.setStatus("not-installed");
2059
+ this.log("Global OpenClaw package removed (or no longer detected).");
2060
+ return;
2061
+ }
2062
+ await this.sleep(this.getUninstallCheckIntervalMs());
2063
+ }
2064
+ if (!managedInstallPresent && lastResult?.running) {
2065
+ void vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Managed package was removed, but the gateway listener still appears to be running. Finish the terminal command or run Status.`);
2066
+ }
2067
+ else {
2068
+ void vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Uninstall is still verifying managed package removal. Finish the terminal command or run Status.`);
2069
+ }
2070
+ }
2071
+ finally {
2072
+ this._gatewayUninstallInProgress = false;
2073
+ this.updateStatusBar(this._status);
2074
+ }
2075
+ }
2076
+ // ============================================================================
2077
+ // Config
2078
+ // ============================================================================
2079
+ /** Path to the OpenClaw config file */
2080
+ get configPath() {
2081
+ return path.join(os.homedir(), ".openclaw", "openclaw.json");
2082
+ }
2083
+ /**
2084
+ * Write a starter openclaw.json if none exists.
2085
+ * Reads the template from base-prompts/gateway/openclaw-starter.json5
2086
+ * (copied to out/ during compile). JSON5 format -- comments supported by OpenClaw.
2087
+ */
2088
+ writeStarterConfig() {
2089
+ const configDir = path.dirname(this.configPath);
2090
+ if (fs.existsSync(this.configPath)) {
2091
+ this.log("Config already exists, skipping starter config");
2092
+ return;
2093
+ }
2094
+ // Find the starter template (check compiled output first, then source for dev)
2095
+ const templateCandidates = [
2096
+ path.join(__dirname, "..", "base-prompts", "gateway", "openclaw-starter.json5"),
2097
+ path.join(__dirname, "..", "..", "base-prompts", "gateway", "openclaw-starter.json5"),
2098
+ ];
2099
+ const templatePath = templateCandidates.find((p) => fs.existsSync(p));
2100
+ if (!templatePath) {
2101
+ this.log("Starter config template not found, skipping");
2102
+ return;
2103
+ }
2104
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
2105
+ // Read template and replace placeholder token with a random one
2106
+ let content = fs.readFileSync(templatePath, "utf-8");
2107
+ const randomToken = `ah-${(0, crypto_1.randomBytes)(24).toString("hex")}`;
2108
+ content = content.replace("__GENERATED_TOKEN__", randomToken);
2109
+ fs.writeFileSync(this.configPath, content, { encoding: "utf-8", mode: IS_WINDOWS ? undefined : 0o600 });
2110
+ this.log(`Wrote starter config to ${this.configPath}`);
2111
+ }
2112
+ /**
2113
+ * Open the OpenClaw config file in the editor.
2114
+ */
2115
+ async openConfig() {
2116
+ if (!fs.existsSync(this.configPath)) {
2117
+ this.writeStarterConfig();
2118
+ }
2119
+ const doc = await vscode.workspace.openTextDocument(this.configPath);
2120
+ await vscode.window.showTextDocument(doc);
2121
+ }
2122
+ async startGatewayViaHiddenWindowsProcess(spawnExe, spawnArgs, logPath, errorLogPath, gatewayEnv) {
2123
+ const argumentLine = (0, windowsCommandLine_js_1.buildWindowsArgumentLine)(spawnArgs);
2124
+ const argumentClause = argumentLine.length > 0
2125
+ ? ` -ArgumentList '${escapePowerShellSingleQuotedString(argumentLine)}'`
2126
+ : "";
2127
+ const psCommand = [
2128
+ `$proc = Start-Process -FilePath '${escapePowerShellSingleQuotedString(spawnExe)}' -WindowStyle Hidden${argumentClause} -RedirectStandardOutput '${escapePowerShellSingleQuotedString(logPath)}' -RedirectStandardError '${escapePowerShellSingleQuotedString(errorLogPath)}' -PassThru`,
2129
+ "Write-Output $proc.Id",
2130
+ ].join("; ");
2131
+ return new Promise((resolve) => {
2132
+ let stdout = "";
2133
+ let stderr = "";
2134
+ const launcher = (0, child_process_1.spawn)("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", psCommand], {
2135
+ windowsHide: true,
2136
+ env: gatewayEnv,
2137
+ });
2138
+ launcher.stdout.on("data", (data) => {
2139
+ stdout += data.toString();
2140
+ });
2141
+ launcher.stderr.on("data", (data) => {
2142
+ stderr += data.toString();
2143
+ });
2144
+ launcher.once("error", (err) => {
2145
+ this.log(`Gateway hidden Windows launcher failed to spawn: ${err}`);
2146
+ resolve(undefined);
2147
+ });
2148
+ launcher.once("exit", (code) => {
2149
+ if (code !== 0) {
2150
+ const detail = stderr.trim() || stdout.trim();
2151
+ this.log(`Gateway hidden Windows launcher exited with code ${code}${detail ? `: ${detail}` : ""}`);
2152
+ resolve(undefined);
2153
+ return;
2154
+ }
2155
+ const startedPid = parseInt(stdout.trim(), 10);
2156
+ if (!Number.isNaN(startedPid)) {
2157
+ this.log(`Gateway hidden Windows process started (PID: ${startedPid})`);
2158
+ resolve(startedPid);
2159
+ return;
2160
+ }
2161
+ this.log("Gateway hidden Windows launcher did not return a PID");
2162
+ resolve(undefined);
2163
+ });
2164
+ });
2165
+ }
2166
+ // ============================================================================
2167
+ // Start
2168
+ // ============================================================================
2169
+ async start() {
2170
+ if (this._gatewayUninstallInProgress) {
2171
+ await this.showGatewayUninstallInProgressPrompt();
2172
+ return;
2173
+ }
2174
+ if (this._startInFlight) {
2175
+ this.setStatus("starting");
2176
+ await this._startInFlight;
2177
+ return;
2178
+ }
2179
+ this.setStatus("starting");
2180
+ const startPromise = this.doStart();
2181
+ this._startInFlight = startPromise;
2182
+ try {
2183
+ await startPromise;
2184
+ }
2185
+ finally {
2186
+ if (this._startInFlight === startPromise) {
2187
+ this._startInFlight = null;
2188
+ }
2189
+ }
2190
+ }
2191
+ async doStart() {
2192
+ // Fast check first: if PID file says running, skip slow detection
2193
+ const port = this.getGatewayPort();
2194
+ const fast = await this.detectGatewayFastState(port);
2195
+ if (fast?.running) {
2196
+ this.log("Gateway already running (fast check)");
2197
+ this._lastDetection = fast;
2198
+ this.setStatus("running");
2199
+ return;
2200
+ }
2201
+ // Full detection needed (finds binary path, checks install status)
2202
+ const detection = await this.detect();
2203
+ const portOccupied = await this.probeGatewayPort(port);
2204
+ if (!detection.installed) {
2205
+ if (portOccupied) {
2206
+ const approved = await this.promptNonManagedPortTakeover(port, "start");
2207
+ if (!approved || (await this.probeGatewayPort(port))) {
2208
+ this.setStatus("error");
2209
+ return;
2210
+ }
2211
+ }
2212
+ const doInstall = await vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Gateway is not installed.`, "Install Now");
2213
+ if (doInstall === "Install Now") {
2214
+ await this.install();
2215
+ }
2216
+ else {
2217
+ // Dismiss leaves start(); we must not keep "starting" (blocks background detect / toggle).
2218
+ this.setStatus("not-installed");
2219
+ }
2220
+ return;
2221
+ }
2222
+ if (detection.running) {
2223
+ this.log("Gateway already running");
2224
+ this.setStatus("running");
2225
+ return;
2226
+ }
2227
+ if (portOccupied) {
2228
+ const approved = await this.promptNonManagedPortTakeover(port, "start");
2229
+ if (!approved || (await this.probeGatewayPort(port))) {
2230
+ this.setStatus("error");
2231
+ return;
2232
+ }
2233
+ }
2234
+ if (!detection.binaryPath) {
2235
+ this.setStatus("error");
2236
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Gateway binary not found. Try reinstalling.`);
2237
+ return;
2238
+ }
2239
+ const binaryPath = detection.binaryPath;
2240
+ // Final attach guard: detection.running was evaluated earlier, but detection and the
2241
+ // stale-lock cleanup below take seconds (CLI `--version`, `gateway stop`), during which a
2242
+ // healthy gateway -- ours from a prior start, or one started via the CLI -- can bind the
2243
+ // port. Re-check now and attach instead of force-respawning, which would kill it.
2244
+ if ((await this.probeGatewayPort(port)) && (await isTrustedGatewayListener(port, binaryPath))) {
2245
+ this.log("Healthy gateway already listening; attaching instead of starting a new one");
2246
+ this._lastDetection = { ...detection, running: true };
2247
+ this.setStatus("running");
2248
+ return;
2249
+ }
2250
+ // Best-effort stale-lock cleanup: OpenClaw can report "gateway already running"
2251
+ // even when no trusted listener is present, for example after a crashed prior
2252
+ // instance. Ask the CLI to stop first so a fresh local start can succeed.
2253
+ await this.stopExistingGatewayBeforeStart(binaryPath);
2254
+ await this.removeStaleGatewayLockBeforeStart(port);
2255
+ // Only force-free the port when something still holds it right before launch (a non-managed
2256
+ // listener, or a straggler from a crashed prior instance). When the port is free -- the
2257
+ // normal case -- omit `--force` so we never kill a gateway that bound during startup.
2258
+ // stop() now reliably reaps our own listener, so `--force` is no longer needed for restarts.
2259
+ const gatewayArgs = ["gateway", "run", "--bind", "loopback", "--port", String(port)];
2260
+ if (await this.probeGatewayPort(port)) {
2261
+ gatewayArgs.push("--force");
2262
+ }
2263
+ let spawnExe = binaryPath;
2264
+ let spawnArgs = gatewayArgs;
2265
+ let spawnShell = false;
2266
+ const isWindowsLaunch = this.isWindowsLaunch();
2267
+ if (isWindowsLaunch) {
2268
+ // Windows lifecycle launch must avoid shell-backed .cmd/.ps1 shims. Those wrappers can
2269
+ // create a visible console window and make detached startup behave like a terminal-owned
2270
+ // session. Repair the managed runtime metadata if needed, then launch node.exe directly.
2271
+ const windowsLaunchRuntime = await this.resolveWindowsManagedGatewayLaunchRuntime(binaryPath);
2272
+ if (!windowsLaunchRuntime) {
2273
+ this.setStatus("installed");
2274
+ const choice = await vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Windows background start requires managed gateway runtime metadata. Reinstall Agent Anywhere to refresh the launch configuration.`, "Reinstall");
2275
+ if (choice === "Reinstall") {
2276
+ await this.install();
2277
+ }
2278
+ return;
2279
+ }
2280
+ spawnExe = windowsLaunchRuntime.nodePath;
2281
+ spawnArgs = [windowsLaunchRuntime.entryScriptPath, ...gatewayArgs];
2282
+ }
2283
+ // Gateway log file -- survives IDE restarts so users can debug issues
2284
+ const logDir = path.dirname(DEFAULT_GATEWAY_LOG_PATH);
2285
+ fs.mkdirSync(logDir, { recursive: true });
2286
+ const logPath = DEFAULT_GATEWAY_LOG_PATH;
2287
+ const errorLogPath = path.join(logDir, "gateway.error.log");
2288
+ this.log(`Starting gateway: ${spawnExe} ${spawnArgs.join(" ")}`);
2289
+ this.log(`Gateway log: ${logPath}`);
2290
+ // Re-reads ~/.agent-hippo/.env and overlays managed secret keys so token rotations
2291
+ // (e.g. switching Slack apps) take effect here instead of reusing the stale process.env
2292
+ // snapshot captured at extension activation.
2293
+ const gatewayEnv = await (0, agentEnvFile_js_1.readGatewayChildProcessEnv)();
2294
+ // The IDE spawns the gateway directly (detached), with no launchd/systemd supervisor. On a
2295
+ // config-triggered restart the gateway would otherwise take the "supervised" path -- exit(0)
2296
+ // expecting a supervisor to relaunch it -- and never come back. Force the in-process restart
2297
+ // path so SIGUSR1 restarts re-listen in the same process and survive (no supervisor needed).
2298
+ gatewayEnv.OPENCLAW_NO_RESPAWN = "1";
2299
+ for (const key of agentEnvFile_js_1.GATEWAY_SECRET_ENV_KEYS) {
2300
+ if (gatewayEnv[key]?.trim()) {
2301
+ this.log(`Gateway env: ${key} present for child process`);
2302
+ }
2303
+ }
2304
+ let startedPid;
2305
+ if (isWindowsLaunch) {
2306
+ startedPid = await this.startGatewayViaHiddenWindowsProcess(spawnExe, spawnArgs, logPath, errorLogPath, gatewayEnv);
2307
+ this.process = null;
2308
+ if (!startedPid) {
2309
+ this.setStatus("error");
2310
+ await showGatewayStartFailedToast(logPath);
2311
+ return;
2312
+ }
2313
+ }
2314
+ else {
2315
+ const logFd = fs.openSync(logPath, "a");
2316
+ this.process = (0, child_process_1.spawn)(spawnExe, spawnArgs, {
2317
+ stdio: ["ignore", logFd, logFd],
2318
+ detached: true, // Persist across IDE restarts
2319
+ env: gatewayEnv,
2320
+ shell: spawnShell,
2321
+ windowsHide: true, // Prevent cmd.exe window appearing on Windows
2322
+ });
2323
+ // Close the log fd in this process -- the child owns it now
2324
+ fs.closeSync(logFd);
2325
+ // Detach: let the gateway keep running after the IDE exits.
2326
+ // The IDE only manages the process lifecycle via explicit Start/Stop commands.
2327
+ this.process.unref();
2328
+ this.process.on("exit", (code) => {
2329
+ this.log(`Gateway process exited with code ${code}`);
2330
+ this.process = null;
2331
+ // Clean up PID file
2332
+ void removeGatewayPidFile();
2333
+ // While start()/doStart readiness runs, that loop owns terminal status (Unix: clean exit
2334
+ // before the port opens must surface as error, not "installed", vs this handler).
2335
+ if (this._startInFlight) {
2336
+ return;
2337
+ }
2338
+ this.setStatus(code !== 0 && code !== null ? "error" : "installed");
2339
+ });
2340
+ }
2341
+ // Wait for gateway readiness in the window progress area. VS Code progress
2342
+ // notifications only support a Cancel button, not an X-only close affordance.
2343
+ await vscode.window.withProgress({
2344
+ location: vscode.ProgressLocation.Window,
2345
+ title: `${exports.AGENT_ANYWHERE_LABEL}: Starting gateway...`,
2346
+ }, async (progress) => {
2347
+ try {
2348
+ const startTime = Date.now();
2349
+ const startupTimeoutMs = this.getStartupTimeoutMs();
2350
+ const launchGraceMs = this.getStartupLaunchGraceMs();
2351
+ const checkInterval = this.getStartupCheckIntervalMs();
2352
+ let elapsed = 0;
2353
+ let lastPercent = 0;
2354
+ let sawLaunchedProcessAlive = false;
2355
+ let loggedLaunchedProcessExit = false;
2356
+ const trackingPid = this.process?.pid ?? startedPid;
2357
+ while (true) {
2358
+ if (await this.probeGatewayPort(port)) {
2359
+ this.setStatus("running");
2360
+ this.log(`Gateway started and ready (${elapsed}ms)`);
2361
+ // Write PID file for fast subsequent status checks
2362
+ const pidForTracking = this.process?.pid ?? startedPid;
2363
+ if (pidForTracking) {
2364
+ await this.writeGatewayPidRecord(pidForTracking, port, binaryPath);
2365
+ this.log(`PID file written: ${GATEWAY_PID_FILE}`);
2366
+ }
2367
+ this.startHealthChecks(port);
2368
+ progress.report({ message: "Ready!", increment: 100 });
2369
+ return;
2370
+ }
2371
+ const processAlive = this.isStartedGatewayProcessAlive(trackingPid);
2372
+ if (trackingPid && processAlive) {
2373
+ sawLaunchedProcessAlive = true;
2374
+ }
2375
+ elapsed = Date.now() - startTime;
2376
+ const withinPrimaryTimeout = elapsed < startupTimeoutMs;
2377
+ const withinLaunchGrace = trackingPid !== undefined &&
2378
+ processAlive &&
2379
+ sawLaunchedProcessAlive &&
2380
+ elapsed < startupTimeoutMs + launchGraceMs;
2381
+ if (trackingPid && sawLaunchedProcessAlive && !processAlive && !isWindowsLaunch) {
2382
+ this.setStatus("error");
2383
+ this.log(`Gateway process exited before opening port ${port}`);
2384
+ await showGatewayStartFailedToast(logPath);
2385
+ return;
2386
+ }
2387
+ if (trackingPid && sawLaunchedProcessAlive && !processAlive && isWindowsLaunch && !loggedLaunchedProcessExit) {
2388
+ loggedLaunchedProcessExit = true;
2389
+ this.log(`Gateway launch process exited before opening port ${port}; continuing to wait for port readiness`);
2390
+ }
2391
+ if (!withinPrimaryTimeout && !withinLaunchGrace) {
2392
+ break;
2393
+ }
2394
+ const denominator = Math.max(startupTimeoutMs + (withinLaunchGrace ? launchGraceMs : 0), 1);
2395
+ const pct = Math.min(90, Math.floor((elapsed / denominator) * 100));
2396
+ const increment = Math.max(0, pct - lastPercent);
2397
+ lastPercent = pct;
2398
+ const progressMessage = trackingPid !== undefined
2399
+ ? processAlive
2400
+ ? `Gateway launched (PID ${trackingPid}), waiting for port ${port}...`
2401
+ : `Waiting for gateway process to initialize...`
2402
+ : `Waiting for port ${port}...`;
2403
+ progress.report({ message: progressMessage, increment });
2404
+ await this.sleep(checkInterval);
2405
+ }
2406
+ // Timeout
2407
+ this.setStatus("error");
2408
+ this.log(`Gateway failed to start within timeout (pid=${trackingPid ?? "n/a"}, alive=${trackingPid !== undefined ? this.isStartedGatewayProcessAlive(trackingPid) : false})`);
2409
+ await showGatewayStartFailedToast(logPath);
2410
+ }
2411
+ catch {
2412
+ this.setStatus("error");
2413
+ this.log("Gateway failed to start");
2414
+ await showGatewayStartFailedToast(logPath);
2415
+ }
2416
+ });
2417
+ }
2418
+ // ============================================================================
2419
+ // Stop
2420
+ // ============================================================================
2421
+ async stop() {
2422
+ this.stopHealthChecks();
2423
+ // Clear bridge UI state immediately so we don't show green "Connected" while
2424
+ // the gateway is being torn down (or if stop fails but the socket was stale).
2425
+ await this.disconnectBridge();
2426
+ if (this.process) {
2427
+ // We started it -- graceful shutdown
2428
+ this.setStatus("stopping");
2429
+ this.log("Stopping gateway...");
2430
+ const port = this.getGatewayPort();
2431
+ const pid = this.process.pid;
2432
+ // The gateway we spawn is a launcher whose CHILD process holds the listening socket.
2433
+ // Capture the actual listener(s) now so we can confirm/reap them after the launcher exits.
2434
+ const listenerPids = await getListeningProcessIdsForPort(port);
2435
+ const observedPids = [
2436
+ ...new Set([pid, ...listenerPids].filter((p) => typeof p === "number" && p > 0)),
2437
+ ];
2438
+ if (IS_WINDOWS && pid) {
2439
+ // On Windows, SIGTERM maps to TerminateProcess which force-kills.
2440
+ // Use taskkill /T to also kill child processes (node spawns).
2441
+ await execAsync(`taskkill /F /T /PID ${pid}`, { timeout: 5_000 }).catch(() => { });
2442
+ }
2443
+ else {
2444
+ // Signal the whole process group (we spawn detached, so the launcher is the group
2445
+ // leader); its listening child shares the group and would otherwise be orphaned.
2446
+ killGatewayProcessGroupOrSelf(this.process, pid, "SIGTERM");
2447
+ }
2448
+ const exited = await new Promise((resolve) => {
2449
+ const timeout = setTimeout(() => resolve(false), 5_000);
2450
+ this.process?.once("exit", () => {
2451
+ clearTimeout(timeout);
2452
+ resolve(true);
2453
+ });
2454
+ });
2455
+ if (!exited && this.process) {
2456
+ this.log("Forcefully killing gateway...");
2457
+ if (IS_WINDOWS && pid) {
2458
+ await execAsync(`taskkill /F /T /PID ${pid}`, { timeout: 5_000 }).catch(() => { });
2459
+ }
2460
+ else {
2461
+ killGatewayProcessGroupOrSelf(this.process, pid, "SIGKILL");
2462
+ }
2463
+ }
2464
+ this.process = null;
2465
+ await removeGatewayPidFile();
2466
+ // The launcher exiting does not guarantee its listening child died and released the port.
2467
+ // Confirm the port is actually free; if not, reap the straggler by port (same mechanism as
2468
+ // the external-stop path) so we never report "stopped" while the port is still held.
2469
+ let freed = await waitForGatewayShutdown(port, observedPids);
2470
+ if (!freed) {
2471
+ this.log("Gateway port still held after process exit; reaping listener(s)...");
2472
+ await killTcpListenersOnPort(port);
2473
+ freed = await waitForGatewayShutdown(port, [
2474
+ ...observedPids,
2475
+ ...(await getListeningProcessIdsForPort(port)),
2476
+ ]);
2477
+ }
2478
+ if (freed) {
2479
+ this.setStatus("installed");
2480
+ this.log("Gateway stopped");
2481
+ }
2482
+ else {
2483
+ this.setStatus("running");
2484
+ this.log("Could not free gateway port after stop");
2485
+ vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not fully stop the gateway. Try stopping it manually.`);
2486
+ }
2487
+ return;
2488
+ }
2489
+ // We didn't start it -- check if something else is running
2490
+ const detection = await this.detect();
2491
+ if (!detection.running) {
2492
+ this.log("Gateway is not running");
2493
+ return;
2494
+ }
2495
+ // Kill external gateway process (CLI stop → pkill/CIM → port-based kill)
2496
+ this.setStatus("stopping");
2497
+ this.log("Stopping externally started gateway...");
2498
+ const stopped = await stopExternalGatewayListener(detection.port, detection.binaryPath, (m) => this.log(m));
2499
+ await removeGatewayPidFile();
2500
+ if (stopped) {
2501
+ this.setStatus("installed");
2502
+ this.log("Gateway stopped");
2503
+ }
2504
+ else {
2505
+ this.setStatus("running");
2506
+ this.log("Could not stop gateway process");
2507
+ vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not stop the gateway. Try stopping it manually.`);
2508
+ }
2509
+ }
2510
+ async restartGateway() {
2511
+ const wasConnected = this._bridgeRef?.isConnected ?? false;
2512
+ await this.disconnectBridge();
2513
+ let shouldStop = this._status === "running";
2514
+ if (!shouldStop) {
2515
+ // Lightweight runtime check to avoid full detect() latency.
2516
+ shouldStop = await (0, index_1.probePort)(this.getGatewayPort(), "127.0.0.1", 250);
2517
+ }
2518
+ if (shouldStop) {
2519
+ await this.stop();
2520
+ }
2521
+ else {
2522
+ this.log("Restart requested while gateway is stopped; skipping stop.");
2523
+ }
2524
+ await this.start();
2525
+ if (wasConnected && this._status === "running") {
2526
+ await this.connectBridge();
2527
+ }
2528
+ }
2529
+ // ============================================================================
2530
+ // Status
2531
+ // ============================================================================
2532
+ async showStatus() {
2533
+ this.setBridgeConnected(this._bridgeRef?.isConnected ?? false);
2534
+ const port = this.getGatewayPort();
2535
+ const detection = this._lastDetection ?? await this.detect();
2536
+ // Build status summary
2537
+ // allow-any-unicode-next-line
2538
+ const gwStatus = detection.running ? "🟢 Running" : detection.installed ? "⚫ Stopped" : "⬜ Not installed";
2539
+ // allow-any-unicode-next-line
2540
+ const bridgeStatus = this._bridgeConnected ? "🟢 Connected" : "⚫ Disconnected";
2541
+ const version = detection.version ? ` (${detection.version})` : "";
2542
+ const message = [
2543
+ `Gateway: ${gwStatus}${version}`,
2544
+ `Bridge: ${bridgeStatus}`,
2545
+ `Port: ${port}`,
2546
+ ].join("\n");
2547
+ if (!detection.installed) {
2548
+ const choice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}\n${message}`, "Set Up");
2549
+ if (choice === "Set Up") {
2550
+ await this.install();
2551
+ }
2552
+ return;
2553
+ }
2554
+ if (!detection.running) {
2555
+ const choice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}\n${message}`, "Connect", "Config", "Uninstall");
2556
+ switch (choice) {
2557
+ case "Connect":
2558
+ await this.start();
2559
+ if (this._status === "running") {
2560
+ await this.connectBridge();
2561
+ }
2562
+ break;
2563
+ case "Config":
2564
+ await this.openConfig();
2565
+ break;
2566
+ case "Uninstall":
2567
+ await this.uninstall();
2568
+ break;
2569
+ }
2570
+ return;
2571
+ }
2572
+ // Running -- show status details with maintenance actions
2573
+ const choice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}\n${message}`, "Web UI", "Config", "Restart", "Reset auth", "Uninstall");
2574
+ switch (choice) {
2575
+ case "Web UI":
2576
+ await this.openWebUI();
2577
+ break;
2578
+ case "Config":
2579
+ await this.openConfig();
2580
+ break;
2581
+ case "Restart":
2582
+ await this.restartGateway();
2583
+ break;
2584
+ case "Reset auth":
2585
+ await this.resetAuth();
2586
+ break;
2587
+ case "Uninstall":
2588
+ await this.uninstall();
2589
+ break;
2590
+ }
2591
+ }
2592
+ async resetAuth() {
2593
+ try {
2594
+ if (!fs.existsSync(this.configPath)) {
2595
+ this.writeStarterConfig();
2596
+ }
2597
+ const raw = fs.readFileSync(this.configPath, "utf-8");
2598
+ const stripped = raw.replace(/^\s*\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1");
2599
+ const parsed = JSON.parse(stripped);
2600
+ if (!parsed.gateway || typeof parsed.gateway !== "object") {
2601
+ parsed.gateway = {};
2602
+ }
2603
+ if (!parsed.gateway.auth || typeof parsed.gateway.auth !== "object") {
2604
+ parsed.gateway.auth = {};
2605
+ }
2606
+ const newToken = `ah-${(0, crypto_1.randomBytes)(24).toString("hex")}`;
2607
+ parsed.gateway.auth.token = newToken;
2608
+ fs.writeFileSync(this.configPath, `${JSON.stringify(parsed, null, 2)}\n`, {
2609
+ encoding: "utf-8",
2610
+ mode: IS_WINDOWS ? undefined : 0o600,
2611
+ });
2612
+ const gwConfig = vscode.workspace.getConfiguration("agentide.gateway");
2613
+ if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
2614
+ try {
2615
+ await gwConfig.update("token", newToken, vscode.ConfigurationTarget.Workspace);
2616
+ }
2617
+ catch (err) {
2618
+ this.log(`Workspace token update skipped: ${err instanceof Error ? err.message : String(err)}`);
2619
+ }
2620
+ }
2621
+ // Always persist user scope so empty-window mode (no workspace) still works.
2622
+ await gwConfig.update("token", newToken, vscode.ConfigurationTarget.Global);
2623
+ await this.restartGateway();
2624
+ void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Auth reset; gateway restarted. If you use the Web UI, paste the new token in Control UI settings.`);
2625
+ }
2626
+ catch (err) {
2627
+ this.log(`Failed to reset auth: ${err instanceof Error ? err.message : String(err)}`);
2628
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Failed to reset auth. Open Config to update it manually.`);
2629
+ }
2630
+ }
2631
+ // ============================================================================
2632
+ // Bridge Connect/Disconnect
2633
+ // ============================================================================
2634
+ /**
2635
+ * Connect the bridge to the running gateway.
2636
+ * Ensures the gateway.url setting is set, then delegates to GatewayService.connect().
2637
+ */
2638
+ async connectBridge() {
2639
+ if (!this._bridgeRef) {
2640
+ this.log("Bridge ref not set -- cannot connect");
2641
+ return;
2642
+ }
2643
+ if (this._gatewayUninstallInProgress) {
2644
+ await this.showGatewayUninstallInProgressPrompt();
2645
+ return;
2646
+ }
2647
+ if (this._bridgeRef.isConnected) {
2648
+ this.log("Bridge already connected");
2649
+ return;
2650
+ }
2651
+ // Fast check: if we already know the gateway is running, skip full detection.
2652
+ // Full detect() shells out to `openclaw --version` which adds 1-3s latency.
2653
+ const port = this.getGatewayPort();
2654
+ if (await (0, index_1.probePort)(port)) {
2655
+ const trustedListener = await isTrustedGatewayListener(port, this._lastDetection?.binaryPath);
2656
+ if (!trustedListener) {
2657
+ const approved = await this.promptNonManagedPortTakeover(port, "connect");
2658
+ if (!approved || (await (0, index_1.probePort)(port))) {
2659
+ if (this._status === "running") {
2660
+ this.setStatus("installed");
2661
+ }
2662
+ return;
2663
+ }
2664
+ }
2665
+ }
2666
+ let isRunning = this._status === "running";
2667
+ if (!isRunning) {
2668
+ // Try fast PID-file check first (~1-2ms)
2669
+ const fast = await detectGatewayFast(port);
2670
+ if (fast?.running) {
2671
+ isRunning = true;
2672
+ this._lastDetection = fast;
2673
+ this.setStatus("running");
2674
+ }
2675
+ }
2676
+ if (!isRunning) {
2677
+ // Slow path: full detection only when necessary
2678
+ const detection = await this.detect();
2679
+ if (!detection.running) {
2680
+ if (!detection.installed) {
2681
+ const choice = await vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Reach your AgentHippo IDE or CLI agents from WhatsApp, Telegram, Slack, Discord, and more - right from your phone or any messaging app. This will also install OpenClaw fork on your computer.`, "Set Up Agent Anywhere");
2682
+ if (choice === "Set Up Agent Anywhere") {
2683
+ await this.install();
2684
+ }
2685
+ return;
2686
+ }
2687
+ await this.start();
2688
+ // Re-detect after start
2689
+ const afterStart = await this.detect();
2690
+ if (!afterStart.running) {
2691
+ this.log("Gateway failed to start, cannot connect");
2692
+ await showGatewayStartFailedToast(DEFAULT_GATEWAY_LOG_PATH);
2693
+ return;
2694
+ }
2695
+ }
2696
+ }
2697
+ // Ensure gateway.url is set
2698
+ const gwConfig = vscode.workspace.getConfiguration("agentide.gateway");
2699
+ let wsUrl = gwConfig.get("url", "").trim();
2700
+ if (!wsUrl) {
2701
+ wsUrl = `ws://127.0.0.1:${port}`;
2702
+ await gwConfig.update("url", wsUrl, vscode.ConfigurationTarget.Global);
2703
+ }
2704
+ // For local gateways, apply gateway.auth.token from ~/.openclaw/openclaw.json.
2705
+ // Prefer workspace scope when a workspace exists; otherwise use user scope in empty-window mode.
2706
+ // Remote URLs: do not override — token must come from settings / env for that host.
2707
+ const localGateway = /127\.0\.0\.1|localhost|\[::1\]/i.test(wsUrl);
2708
+ const fileToken = this.readGatewayToken();
2709
+ if (fileToken && localGateway) {
2710
+ if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
2711
+ await gwConfig.update("token", fileToken, vscode.ConfigurationTarget.Workspace);
2712
+ this.log("Gateway token applied from openclaw.json (workspace scope)");
2713
+ }
2714
+ else {
2715
+ await gwConfig.update("token", fileToken, vscode.ConfigurationTarget.Global);
2716
+ this.log("Gateway token applied from openclaw.json (global scope; no workspace open)");
2717
+ }
2718
+ }
2719
+ await this._bridgeRef.connect();
2720
+ this.setBridgeConnected(this._bridgeRef.isConnected);
2721
+ // Ensure the visual state matches the actual gateway port state.
2722
+ // This prevents a race where the bridge connects but `_status` is still "installed".
2723
+ const alive = await (0, index_1.probePort)(port);
2724
+ if (alive) {
2725
+ if (this._status !== "running") {
2726
+ this.setStatus("running");
2727
+ }
2728
+ }
2729
+ else {
2730
+ if (this._status !== "installed") {
2731
+ this.setStatus("installed");
2732
+ }
2733
+ }
2734
+ this.updateStatusBar(this._status);
2735
+ this.log("Bridge connected");
2736
+ }
2737
+ /**
2738
+ * Disconnect the bridge from the gateway.
2739
+ */
2740
+ async disconnectBridge() {
2741
+ if (!this._bridgeRef) {
2742
+ return;
2743
+ }
2744
+ await this._bridgeRef.disconnect();
2745
+ this.setBridgeConnected(false);
2746
+ this.updateStatusBar(this._status);
2747
+ this.log("Bridge disconnected");
2748
+ }
2749
+ // ============================================================================
2750
+ // Web UI
2751
+ // ============================================================================
2752
+ /**
2753
+ * Open the OpenClaw Gateway web UI in the default browser.
2754
+ * The web UI is served by the gateway on the same port (default: 18789).
2755
+ * Use it to configure channels, monitor sessions, and manage agents.
2756
+ */
2757
+ async openWebUI() {
2758
+ const port = this.getGatewayPort();
2759
+ // Quick port probe (~10ms) instead of full detect (~1-3s)
2760
+ const running = this._status === "running" || await (0, index_1.probePort)(port);
2761
+ if (!running) {
2762
+ const choice = await vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Gateway is not running. Start it first to use the web UI.`, "Start Now");
2763
+ if (choice === "Start Now") {
2764
+ await this.start();
2765
+ }
2766
+ return;
2767
+ }
2768
+ // Pass token in the URL fragment so the Control UI can import it (applySettingsFromUrl).
2769
+ // Query ?token= is intentionally stripped by OpenClaw without applying (referrer/logs).
2770
+ const tokenFromSettings = (vscode.workspace.getConfiguration("agentide.gateway").get("token") ?? "").trim();
2771
+ const token = this.readGatewayToken() ?? (tokenFromSettings || undefined);
2772
+ const hash = token ? `#token=${encodeURIComponent(token)}` : "";
2773
+ const url = `http://127.0.0.1:${port}/${hash}`;
2774
+ await vscode.env.openExternal(vscode.Uri.parse(url));
2775
+ }
2776
+ // ============================================================================
2777
+ // Health Checks
2778
+ // ============================================================================
2779
+ startHealthChecks(port) {
2780
+ this.stopHealthChecks();
2781
+ this.healthCheckTimer = setInterval(async () => {
2782
+ const alive = await (0, index_1.probePort)(port);
2783
+ if (!alive) {
2784
+ this.log("Health check failed -- gateway is no longer running");
2785
+ this.stopHealthChecks();
2786
+ this.process = null;
2787
+ this.setStatus("installed");
2788
+ }
2789
+ }, HEALTH_CHECK_INTERVAL_MS);
2790
+ }
2791
+ stopHealthChecks() {
2792
+ if (this.healthCheckTimer) {
2793
+ clearInterval(this.healthCheckTimer);
2794
+ this.healthCheckTimer = null;
2795
+ }
2796
+ }
2797
+ // ============================================================================
2798
+ // Helpers
2799
+ // ============================================================================
2800
+ /**
2801
+ * Build the web Control UI if it's missing.
2802
+ * Resolves the gateway package root from the binary path and runs `pnpm ui:build`.
2803
+ * Best-effort: logs failures but doesn't block the install flow.
2804
+ */
2805
+ async ensureWebUIBuilt(binaryPath) {
2806
+ if (!binaryPath) {
2807
+ return;
2808
+ }
2809
+ try {
2810
+ const pkgRoot = resolvePackageRootFromBinary(binaryPath);
2811
+ if (!pkgRoot) {
2812
+ return;
2813
+ }
2814
+ const uiIndex = path.join(pkgRoot, "dist", "control-ui", "index.html");
2815
+ if (fs.existsSync(uiIndex)) {
2816
+ return; // Already built
2817
+ }
2818
+ this.log("Web UI not built - building now...");
2819
+ // On Windows, npm/pnpm install as .cmd wrappers that need a shell to execute.
2820
+ const uiBuildCmd = IS_WINDOWS ? "pnpm.cmd ui:build" : "pnpm ui:build";
2821
+ await execAsync(uiBuildCmd, {
2822
+ cwd: pkgRoot,
2823
+ timeout: 60_000,
2824
+ env: { ...process.env },
2825
+ ...(IS_WINDOWS ? { shell: "cmd.exe" } : {}),
2826
+ });
2827
+ this.log("Web UI built successfully");
2828
+ }
2829
+ catch (err) {
2830
+ this.log(`Web UI build failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2831
+ }
2832
+ }
2833
+ async promptNonManagedPortTakeover(port, intent) {
2834
+ const action = intent === "connect" ? "connect Agent Anywhere" : "start Agent Anywhere";
2835
+ const choice = await vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Port ${port} is in use by a non-managed process. Stop that process and ${action}?`, { modal: true }, "Yes", "No");
2836
+ if (choice !== "Yes") {
2837
+ this.log(`User declined non-managed port takeover on ${port}`);
2838
+ return false;
2839
+ }
2840
+ this.log(`User approved non-managed port takeover on ${port}`);
2841
+ const priorPids = new Set(await getListeningProcessIdsForPort(port));
2842
+ const stopped = await stopExternalGatewayListener(port, undefined, (m) => this.log(m));
2843
+ if (!stopped) {
2844
+ // A supervised daemon (launchd/systemd/scheduled task) restarts itself when killed, so a
2845
+ // fresh PID re-takes the port. Detect that and tell the user the truth — killing the
2846
+ // process can't win against the supervisor's auto-restart; the service must be uninstalled.
2847
+ const currentPids = await getListeningProcessIdsForPort(port);
2848
+ const supervisedRespawn = priorPids.size > 0 && currentPids.some((pid) => !priorPids.has(pid));
2849
+ if (supervisedRespawn) {
2850
+ vscode.window.showWarningMessage(`${exports.AGENT_ANYWHERE_LABEL}: Port ${port} is held by a self-restarting gateway service. Run \`openclaw daemon uninstall\` to remove it, then try again.`);
2851
+ }
2852
+ else {
2853
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not free port ${port}. Stop that process manually and try again.`);
2854
+ }
2855
+ return false;
2856
+ }
2857
+ return true;
2858
+ }
2859
+ async ensureManagedGatewayWrapperState(binaryPath, createOpenclawAlias = false) {
2860
+ try {
2861
+ const record = readManagedGatewayInstallRecord();
2862
+ const runtimeSpec = await resolveManagedGatewayRuntimeSpec(binaryPath, record);
2863
+ if (!runtimeSpec) {
2864
+ return undefined;
2865
+ }
2866
+ const { launcherPath, packageRoot, nodePath, entryScriptPath } = runtimeSpec;
2867
+ const wrapperPath = writeManagedGatewayWrapper(nodePath, entryScriptPath, MANAGED_GATEWAY_WRAPPER_PATH);
2868
+ let openclawWrapperPath;
2869
+ if (createOpenclawAlias ||
2870
+ isAgentAnywhereManagedWrapper(MANAGED_OPENCLAW_WRAPPER_PATH)) {
2871
+ openclawWrapperPath = writeManagedGatewayWrapper(nodePath, entryScriptPath, MANAGED_OPENCLAW_WRAPPER_PATH);
2872
+ }
2873
+ writeManagedGatewayInstallRecord({
2874
+ ...record,
2875
+ nodePath,
2876
+ cliPath: launcherPath,
2877
+ entryScriptPath,
2878
+ packageRoot,
2879
+ wrapperPath,
2880
+ wrapperDir: MANAGED_GATEWAY_WRAPPER_DIR,
2881
+ openclawWrapperPath,
2882
+ updatedAt: new Date().toISOString(),
2883
+ });
2884
+ return wrapperPath;
2885
+ }
2886
+ catch (err) {
2887
+ this.log(`Managed gateway wrapper setup failed: ${err instanceof Error ? err.message : String(err)}`);
2888
+ return undefined;
2889
+ }
2890
+ }
2891
+ async resolveWindowsManagedGatewayLaunchRuntime(binaryPath) {
2892
+ if (!IS_WINDOWS) {
2893
+ return undefined;
2894
+ }
2895
+ // Best effort: backfill manifest + wrapper state from an existing managed package install.
2896
+ await this.ensureManagedGatewayWrapperState(binaryPath);
2897
+ const runtimeSpec = await resolveManagedGatewayRuntimeSpec(binaryPath);
2898
+ if (!runtimeSpec) {
2899
+ return undefined;
2900
+ }
2901
+ return {
2902
+ nodePath: runtimeSpec.nodePath,
2903
+ entryScriptPath: runtimeSpec.entryScriptPath,
2904
+ };
2905
+ }
2906
+ /**
2907
+ * Read the gateway auth token from ~/.openclaw/openclaw.json.
2908
+ * Parses JSON5 loosely (strip comments, then JSON.parse) to avoid a dep.
2909
+ */
2910
+ readGatewayToken() {
2911
+ try {
2912
+ if (!fs.existsSync(this.configPath)) {
2913
+ return undefined;
2914
+ }
2915
+ const raw = fs.readFileSync(this.configPath, "utf-8");
2916
+ // Strip single-line comments (// ...) for basic JSON5 compat
2917
+ const stripped = raw.replace(/^\s*\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1");
2918
+ const parsed = JSON.parse(stripped);
2919
+ const token = parsed?.gateway?.auth?.token;
2920
+ return typeof token === "string" && token.trim() ? token.trim() : undefined;
2921
+ }
2922
+ catch (err) {
2923
+ this.log(`Could not read gateway token: ${err}`);
2924
+ return undefined;
2925
+ }
2926
+ }
2927
+ setStatus(status) {
2928
+ this._status = status;
2929
+ this.updateStatusBar(status);
2930
+ this._onStatusChange.fire(status);
2931
+ }
2932
+ updateStatusBar(status) {
2933
+ const hidden = this.getConfig("hidden", false);
2934
+ if (hidden) {
2935
+ this.statusBarItem.hide();
2936
+ return;
2937
+ }
2938
+ switch (status) {
2939
+ case "not-installed":
2940
+ this.statusBarItem.text = `$(cloud-download) ${exports.AGENT_ANYWHERE_LABEL}`;
2941
+ this.statusBarItem.tooltip = `${exports.AGENT_ANYWHERE_LABEL}: Reach your agents from WhatsApp, Telegram, Slack (click to set up)`;
2942
+ this.statusBarItem.color = undefined;
2943
+ this.statusBarItem.backgroundColor = undefined;
2944
+ break;
2945
+ case "installed":
2946
+ this.statusBarItem.text = `$(debug-disconnect) ${exports.AGENT_ANYWHERE_LABEL}`;
2947
+ this.statusBarItem.tooltip = `${exports.AGENT_ANYWHERE_LABEL}: Installed, not running (click to start)`;
2948
+ this.statusBarItem.color = undefined;
2949
+ this.statusBarItem.backgroundColor = undefined;
2950
+ break;
2951
+ case "starting":
2952
+ this.statusBarItem.text = `$(sync~spin) ${exports.AGENT_ANYWHERE_LABEL}`;
2953
+ this.statusBarItem.tooltip = this._gatewayInstallInProgress
2954
+ ? `${exports.AGENT_ANYWHERE_LABEL}: ${this._installProgressMessage ?? "Installing... (see Setup terminal)"}`
2955
+ : `${exports.AGENT_ANYWHERE_LABEL}: Starting...`;
2956
+ this.statusBarItem.color = undefined;
2957
+ this.statusBarItem.backgroundColor = undefined;
2958
+ break;
2959
+ case "running":
2960
+ if (this._bridgeConnected) {
2961
+ this.statusBarItem.text = `$(broadcast) ${exports.AGENT_ANYWHERE_LABEL}`;
2962
+ this.statusBarItem.tooltip = `${exports.AGENT_ANYWHERE_LABEL}: Connected (click for options)`;
2963
+ this.statusBarItem.color = new vscode.ThemeColor("testing.iconPassed");
2964
+ }
2965
+ else {
2966
+ this.statusBarItem.text = `$(debug-disconnect) ${exports.AGENT_ANYWHERE_LABEL}`;
2967
+ this.statusBarItem.tooltip = `${exports.AGENT_ANYWHERE_LABEL}: Disconnected (click to connect)`;
2968
+ this.statusBarItem.color = undefined;
2969
+ }
2970
+ this.statusBarItem.backgroundColor = undefined;
2971
+ break;
2972
+ case "stopping":
2973
+ this.statusBarItem.text = `$(sync~spin) ${exports.AGENT_ANYWHERE_LABEL}`;
2974
+ this.statusBarItem.tooltip = this._gatewayUninstallInProgress
2975
+ ? `${exports.AGENT_ANYWHERE_LABEL}: Uninstalling... (see Uninstall terminal)`
2976
+ : `${exports.AGENT_ANYWHERE_LABEL}: Stopping...`;
2977
+ this.statusBarItem.color = undefined;
2978
+ this.statusBarItem.backgroundColor = undefined;
2979
+ break;
2980
+ case "error":
2981
+ this.statusBarItem.text = `$(warning) ${exports.AGENT_ANYWHERE_LABEL}`;
2982
+ this.statusBarItem.tooltip = `${exports.AGENT_ANYWHERE_LABEL}: Error (click for details)`;
2983
+ this.statusBarItem.color = new vscode.ThemeColor("errorForeground");
2984
+ this.statusBarItem.backgroundColor = undefined;
2985
+ break;
2986
+ }
2987
+ this.statusBarItem.show();
2988
+ }
2989
+ getConfig(key, defaultValue) {
2990
+ return vscode.workspace
2991
+ .getConfiguration("agentide.anywhere")
2992
+ .get(key, defaultValue);
2993
+ }
2994
+ /**
2995
+ * True when the global npm tree still has `openclaw` installed.
2996
+ * This is used by uninstall flow to distinguish managed npm installs
2997
+ * from unrelated/external binaries still present on PATH.
2998
+ */
2999
+ async hasGlobalNpmOpenclawTree() {
3000
+ const managedRoots = await getManagedOpenclawRoots();
3001
+ return managedRoots.length > 0;
3002
+ }
3003
+ readGatewayPortFromOpenclawJson() {
3004
+ try {
3005
+ if (!fs.existsSync(this.configPath)) {
3006
+ return undefined;
3007
+ }
3008
+ const raw = fs.readFileSync(this.configPath, "utf-8");
3009
+ const stripped = raw.replace(/^\s*\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1");
3010
+ const parsed = JSON.parse(stripped);
3011
+ const p = parsed?.gateway?.port;
3012
+ if (typeof p === "number" && Number.isFinite(p)) {
3013
+ const n = Math.floor(p);
3014
+ if (n >= 1 && n <= 65535) {
3015
+ return n;
3016
+ }
3017
+ }
3018
+ if (typeof p === "string" && /^\d+$/.test(p)) {
3019
+ const n = parseInt(p, 10);
3020
+ if (n >= 1 && n <= 65535) {
3021
+ return n;
3022
+ }
3023
+ }
3024
+ return undefined;
3025
+ }
3026
+ catch {
3027
+ return undefined;
3028
+ }
3029
+ }
3030
+ /**
3031
+ * Port used for local gateway probe/start/stop/Web UI. When openclaw.json sets gateway.port,
3032
+ * that wins so the IDE matches the running OpenClaw process (settings default alone can miss).
3033
+ */
3034
+ getGatewayPort() {
3035
+ const fromFile = this.readGatewayPortFromOpenclawJson();
3036
+ if (fromFile !== undefined) {
3037
+ return fromFile;
3038
+ }
3039
+ return this.getConfig("port", DEFAULT_PORT);
3040
+ }
3041
+ log(message) {
3042
+ this.outputChannel.appendLine(`[${exports.AGENT_ANYWHERE_LABEL}] ${message}`);
3043
+ }
3044
+ dispose() {
3045
+ this.stopHealthChecks();
3046
+ // Don't kill the gateway on IDE close -- it's detached and persists.
3047
+ // Users stop it explicitly via the Stop command.
3048
+ this.process = null;
3049
+ this.statusBarItem.dispose();
3050
+ this._onStatusChange.dispose();
3051
+ for (const d of this.disposables) {
3052
+ d.dispose();
3053
+ }
3054
+ }
3055
+ }
3056
+ exports.GatewayLifecycleManager = GatewayLifecycleManager;