@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.
- package/LICENSE +21 -0
- package/bin/agenthippo.js +2 -0
- package/out/agentpack/github.js +547 -0
- package/out/agentpack/index.js +66 -0
- package/out/agentpack/install.js +297 -0
- package/out/agentpack/manager.js +441 -0
- package/out/agentpack/manifest.js +135 -0
- package/out/agentpack/paths.js +475 -0
- package/out/agentpack/publish.js +248 -0
- package/out/agentpack/stores.js +554 -0
- package/out/agentpack/types.js +5 -0
- package/out/analytics/dashboard/artifactManager.js +439 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +159 -0
- package/out/analytics/dashboard/fleetOrchestrator.js +142 -0
- package/out/analytics/dashboard/fleetRegistry.js +79 -0
- package/out/analytics/dashboard/fleetRowDefaults.js +87 -0
- package/out/analytics/dashboard/fleetScriptAdapter.js +146 -0
- package/out/analytics/dashboard/fleetStatusUtils.js +30 -0
- package/out/analytics/dashboard/fleetTitleUtils.js +68 -0
- package/out/analytics/dashboard/fleetTypes.js +29 -0
- package/out/analytics/dashboard/index.js +17 -0
- package/out/analytics/dashboard/indexingService.js +422 -0
- package/out/analytics/dashboard/localFleetAdapter.js +222 -0
- package/out/analytics/dashboard/performanceMonitor.js +76 -0
- package/out/analytics/dashboard/processManager.js +209 -0
- package/out/analytics/dashboard/provider.js +3885 -0
- package/out/analytics/dashboard/queryRunner.js +994 -0
- package/out/analytics/dashboard/refreshPolicy.js +16 -0
- package/out/analytics/dashboard/scheduledFleetFingerprint.js +22 -0
- package/out/analytics/dashboard/scheduledFleetStorage.js +241 -0
- package/out/analytics/dashboard/scheduledFleetUtils.js +22 -0
- package/out/analytics/dashboard/sessionEventFleetStats.js +118 -0
- package/out/analytics/dashboard/sessionJsonlFleet.js +204 -0
- package/out/analytics/dashboard/teamViewDiscovery.js +106 -0
- package/out/analytics/dashboard/viewsConfigMerge.js +221 -0
- package/out/analytics/dashboard/webview/analytics-panel.html +155 -0
- package/out/analytics/dashboard/webview/codicon.ttf +0 -0
- package/out/analytics/dashboard/webview/index.html +78 -0
- package/out/analytics/dashboard/webview/panel.js +2295 -0
- package/out/analytics/dashboard/webview/runtime-bridge.js +82 -0
- package/out/analytics/dashboard/webview/styles.css +1009 -0
- package/out/analytics/lifecycle.js +43 -0
- package/out/analytics/service.js +1279 -0
- package/out/analytics/sessionEvents.js +231 -0
- package/out/auth/bootstrapApply.js +72 -0
- package/out/auth/brokerClient.js +126 -0
- package/out/auth/callbackUri.js +59 -0
- package/out/auth/callbackUriParse.js +59 -0
- package/out/auth/githubStoreGate.js +70 -0
- package/out/auth/loopbackAuthServer.js +165 -0
- package/out/auth/pkce.js +9 -0
- package/out/auth/provider.js +239 -0
- package/out/auth/sessionStore.js +193 -0
- package/out/auth/signInFlow.js +182 -0
- package/out/auth/types.js +12 -0
- package/out/auth-broker/config.js +62 -0
- package/out/auth-broker/jwt.js +105 -0
- package/out/auth-broker/pkce.js +57 -0
- package/out/auth-broker/server.js +539 -0
- package/out/auth-broker/store.js +273 -0
- package/out/base-prompts/gateway/openclaw-starter.json5 +144 -0
- package/out/base-prompts/gateway/slack-socket-mode-minimal.manifest.json +44 -0
- package/out/base-prompts/mcp-servers/.mcp.json +13 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/README.md +142 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/mcp.json +13 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/package-lock.json +1192 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/package.json +23 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/run-with-env.sh +12 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/sdk-test.js +102 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/simple-test.js +137 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-client.js +118 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/scripts/test-server.js +209 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/src/client.ts +431 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/src/index.ts +270 -0
- package/out/base-prompts/mcp-servers/langfuse-analytics/tsconfig.json +18 -0
- package/out/base-prompts/rules/AGENTS.md +204 -0
- package/out/base-prompts/skills/agent-browser/SKILL.md +102 -0
- package/out/base-prompts/skills/agent-builder/SKILL.md +73 -0
- package/out/base-prompts/skills/agent-builder/references/agent-pack.md +63 -0
- package/out/base-prompts/skills/agent-builder/references/crewai.md +66 -0
- package/out/base-prompts/skills/agent-builder/references/langchain.md +60 -0
- package/out/base-prompts/skills/agent-builder/references/raw-sdk.md +73 -0
- package/out/base-prompts/skills/agent-engine-creator/SKILL.md +586 -0
- package/out/base-prompts/skills/agent-engine-installer/SKILL.md +106 -0
- package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.mjs +259 -0
- package/out/base-prompts/skills/agent-engine-installer/scripts/install-engine.ps1 +189 -0
- package/out/base-prompts/skills/agenthippo-cli/SKILL.md +69 -0
- package/out/base-prompts/skills/agenthippo-cli/scripts/install.sh +63 -0
- package/out/base-prompts/skills/agentpack-creator/SKILL.md +375 -0
- package/out/base-prompts/skills/agentpack-creator/references/agent-templates.md +261 -0
- package/out/base-prompts/skills/agentpack-creator/references/agents-md-guide.md +195 -0
- package/out/base-prompts/skills/agentpack-creator/scripts/init_pack.py +391 -0
- package/out/base-prompts/skills/agentpack-creator/scripts/init_pack_test.py +294 -0
- package/out/base-prompts/skills/agentpack-testbench/SKILL.md +135 -0
- package/out/base-prompts/skills/algorithmic-art/LICENSE.txt +202 -0
- package/out/base-prompts/skills/algorithmic-art/SKILL.md +405 -0
- package/out/base-prompts/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/out/base-prompts/skills/algorithmic-art/templates/viewer.html +599 -0
- package/out/base-prompts/skills/debug-agent/SKILL.md +83 -0
- package/out/base-prompts/skills/desktop-automation/SKILL.md +166 -0
- package/out/base-prompts/skills/desktop-automation/references/agenthippo-ui-recipes.md +441 -0
- package/out/base-prompts/skills/desktop-automation/scripts/windows-desktop-uia.psm1 +280 -0
- package/out/base-prompts/skills/docx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/docx/SKILL.md +197 -0
- package/out/base-prompts/skills/docx/docx-js.md +350 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/mce/mc.xsd +75 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/out/base-prompts/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/pack.py +159 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/unpack.py +29 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validate.py +69 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/base.py +951 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/docx.py +274 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/out/base-prompts/skills/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/out/base-prompts/skills/docx/ooxml.md +610 -0
- package/out/base-prompts/skills/docx/scripts/__init__.py +1 -0
- package/out/base-prompts/skills/docx/scripts/document.py +1276 -0
- package/out/base-prompts/skills/docx/scripts/templates/comments.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/templates/people.xml +3 -0
- package/out/base-prompts/skills/docx/scripts/utilities.py +374 -0
- package/out/base-prompts/skills/gateway-outbound/SKILL.md +136 -0
- package/out/base-prompts/skills/mcp-builder/LICENSE.txt +202 -0
- package/out/base-prompts/skills/mcp-builder/SKILL.md +328 -0
- package/out/base-prompts/skills/mcp-builder/reference/evaluation.md +602 -0
- package/out/base-prompts/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/out/base-prompts/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/out/base-prompts/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/out/base-prompts/skills/mcp-builder/scripts/connections.py +151 -0
- package/out/base-prompts/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/out/base-prompts/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/out/base-prompts/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/out/base-prompts/skills/openclaw/SKILL.md +406 -0
- package/out/base-prompts/skills/openclaw/scripts/run-smoke.sh +61 -0
- package/out/base-prompts/skills/pdf/LICENSE.txt +30 -0
- package/out/base-prompts/skills/pdf/SKILL.md +294 -0
- package/out/base-prompts/skills/pdf/forms.md +205 -0
- package/out/base-prompts/skills/pdf/reference.md +612 -0
- package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/out/base-prompts/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/out/base-prompts/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/out/base-prompts/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/out/base-prompts/skills/pdf/scripts/create_validation_image.py +41 -0
- package/out/base-prompts/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/out/base-prompts/skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/out/base-prompts/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/out/base-prompts/skills/pptx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/pptx/SKILL.md +484 -0
- package/out/base-prompts/skills/pptx/html2pptx.md +625 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/out/base-prompts/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/pack.py +159 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/unpack.py +29 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validate.py +69 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/base.py +951 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/out/base-prompts/skills/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/out/base-prompts/skills/pptx/ooxml.md +427 -0
- package/out/base-prompts/skills/pptx/scripts/html2pptx.js +984 -0
- package/out/base-prompts/skills/pptx/scripts/inventory.py +1020 -0
- package/out/base-prompts/skills/pptx/scripts/rearrange.py +231 -0
- package/out/base-prompts/skills/pptx/scripts/replace.py +385 -0
- package/out/base-prompts/skills/pptx/scripts/thumbnail.py +450 -0
- package/out/base-prompts/skills/prompt-lab/SKILL.md +149 -0
- package/out/base-prompts/skills/prompt-lab/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/skill-creator/LICENSE.txt +202 -0
- package/out/base-prompts/skills/skill-creator/SKILL.md +371 -0
- package/out/base-prompts/skills/skill-creator/references/output-patterns.md +82 -0
- package/out/base-prompts/skills/skill-creator/references/workflows.md +28 -0
- package/out/base-prompts/skills/skill-creator/scripts/init_skill.py +303 -0
- package/out/base-prompts/skills/skill-creator/scripts/package_skill.py +110 -0
- package/out/base-prompts/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/out/base-prompts/skills/skill-installer/SKILL.md +251 -0
- package/out/base-prompts/skills/skill-installer/references/store-api.md +219 -0
- package/out/base-prompts/skills/spotlight-dashboard/SKILL.md +83 -0
- package/out/base-prompts/skills/spotlight-insights/SKILL.md +101 -0
- package/out/base-prompts/skills/spotlight-optimize/SKILL.md +93 -0
- package/out/base-prompts/skills/spotlight-optimize/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/spotlight-query/SKILL.md +314 -0
- package/out/base-prompts/skills/spotlight-query/references/otel-schema.md +163 -0
- package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/spotlight-query/scripts/query-analytics.test.sh +744 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/sessions/sess-test-1.jsonl +14 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/.duckdb-index.json +7 -0
- package/out/base-prompts/skills/spotlight-query/scripts/test-fixtures/analytics/traces/sample.jsonl +5 -0
- package/out/base-prompts/skills/spotlight-report/SKILL.md +86 -0
- package/out/base-prompts/skills/spotlight-visualize/SKILL.md +92 -0
- package/out/base-prompts/skills/spotlight-visualize/scripts/query-analytics.sh +2551 -0
- package/out/base-prompts/skills/xlsx/LICENSE.txt +30 -0
- package/out/base-prompts/skills/xlsx/SKILL.md +289 -0
- package/out/base-prompts/skills/xlsx/recalc.py +178 -0
- package/out/branding.js +23 -0
- package/out/cli/commands/anywhere.js +66 -0
- package/out/cli/commands/ask.js +243 -0
- package/out/cli/commands/auth-broker.js +24 -0
- package/out/cli/commands/auth.js +219 -0
- package/out/cli/commands/chat.js +354 -0
- package/out/cli/commands/deploy.js +223 -0
- package/out/cli/commands/gateway.js +160 -0
- package/out/cli/commands/pack.js +220 -0
- package/out/cli/commands/probe-custom-engine.js +82 -0
- package/out/cli/commands/serve.js +583 -0
- package/out/cli/commands/store.js +919 -0
- package/out/cli/credential-context.js +113 -0
- package/out/cli/deploy-init.js +2087 -0
- package/out/cli/deps.js +238 -0
- package/out/cli/index.js +390 -0
- package/out/cli/pack-loader.js +376 -0
- package/out/cli/run-custom-engine-runner.js +318 -0
- package/out/cli/runtime.js +409 -0
- package/out/cli/serve-hardening.js +325 -0
- package/out/cli/state-sync.js +135 -0
- package/out/cli/utils.js +220 -0
- package/out/cli/vscode-shim.js +86 -0
- package/out/engines/base.js +159 -0
- package/out/engines/claude.js +1215 -0
- package/out/engines/codex.js +759 -0
- package/out/engines/custom.js +632 -0
- package/out/engines/customEngineIpc.js +95 -0
- package/out/engines/customEngineRunnerEmbed.js +107 -0
- package/out/engines/customRegistry.js +274 -0
- package/out/engines/embeddedCustomEngineRunner.generated.js +9 -0
- package/out/engines/engineApiKey.js +138 -0
- package/out/engines/factory.js +45 -0
- package/out/engines/install.js +372 -0
- package/out/engines/persistentEngineWorker.js +320 -0
- package/out/engines/publish.js +341 -0
- package/out/engines/types.js +17 -0
- package/out/examples/run-headless-repl.js +291 -0
- package/out/examples/run-headless.js +187 -0
- package/out/extension.js +4472 -0
- package/out/gateway/bridge.js +805 -0
- package/out/gateway/cli-agent-id.js +36 -0
- package/out/gateway/client.js +303 -0
- package/out/gateway/client.test.js +395 -0
- package/out/gateway/gateway-scope-errors.js +14 -0
- package/out/gateway/gatewayBackup.js +76 -0
- package/out/gateway/gatewayRouteIndex.js +371 -0
- package/out/gateway/gatewaySessionStore.js +243 -0
- package/out/gateway/index.js +31 -0
- package/out/gateway/lifecycle.js +3056 -0
- package/out/gateway/openclaw-channel-routing.js +317 -0
- package/out/gateway/outboundHandles.js +373 -0
- package/out/gateway/outboundPaths.js +72 -0
- package/out/gateway/outboundSend.js +254 -0
- package/out/gateway/protocol.js +48 -0
- package/out/gateway/resetMappings.js +52 -0
- package/out/gateway/service.js +1244 -0
- package/out/gateway/sessionRouting.js +77 -0
- package/out/headless/index.js +505 -0
- package/out/headless/sync.js +473 -0
- package/out/mcp/publish.js +143 -0
- package/out/mcp.js +597 -0
- package/out/permissions.js +108 -0
- package/out/permissions.mapping.yaml +68 -0
- package/out/registry/index.js +332 -0
- package/out/remote/handleRemoteChat.js +122 -0
- package/out/remote/remotePackStatusBar.js +103 -0
- package/out/remote/serveClient.js +216 -0
- package/out/remoteSkills.js +191 -0
- package/out/runtime/index.js +32 -0
- package/out/runtime/interfaces/config-provider.js +32 -0
- package/out/runtime/interfaces/edit-tracker.js +22 -0
- package/out/runtime/interfaces/index.js +29 -0
- package/out/runtime/interfaces/logger.js +40 -0
- package/out/runtime/interfaces/permission-handler.js +66 -0
- package/out/runtime/interfaces/session-store.js +342 -0
- package/out/runtime/vscode/index.js +20 -0
- package/out/runtime/vscode/vscode-config-provider.js +77 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +120 -0
- package/out/runtime/vscode/vscode-logger.js +30 -0
- package/out/runtime/vscode/vscode-permission-handler.js +72 -0
- package/out/runtime/vscode/vscode-session-store.js +22 -0
- package/out/scripts/agentide-litellm.ps1 +501 -0
- package/out/scripts/agentide-litellm.sh +700 -0
- package/out/scripts/agentide_litellm_hooks_v1.py +604 -0
- package/out/scripts/analytics/agent-fleet-widget.js +954 -0
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +160 -0
- package/out/scripts/analytics/default-seed-scripts.json +12 -0
- package/out/scripts/analytics/duckdbLocator.js +29 -0
- package/out/scripts/analytics/fleet-local-adapter.js +246 -0
- package/out/scripts/analytics/fleet-workflow.sh +263 -0
- package/out/scripts/analytics/generate-default-charts.py +685 -0
- package/out/scripts/analytics/hotspot-analysis-widget.js +909 -0
- package/out/scripts/analytics/index-traces.py +1394 -0
- package/out/scripts/analytics/install-analytics.sh +286 -0
- package/out/scripts/analytics/live-fleet-4-collector.cjs +315 -0
- package/out/scripts/analytics/live-fleet-4-daemon.mjs +134 -0
- package/out/scripts/analytics/live-fleet-4-widget.cjs +433 -0
- package/out/scripts/analytics/otel-collector.yaml +55 -0
- package/out/scripts/analytics/query-analytics.ps1 +396 -0
- package/out/scripts/analytics/query-analytics.sh +2551 -0
- package/out/scripts/analytics/session-comparison-widget.js +1468 -0
- package/out/scripts/analytics/session-timeline-widget.js +2137 -0
- package/out/scripts/analytics/setup-analytics.ps1 +432 -0
- package/out/scripts/analytics/setup-analytics.sh +854 -0
- package/out/scripts/analytics/start-analytics.ps1 +169 -0
- package/out/scripts/analytics/start-analytics.sh +323 -0
- package/out/scripts/analytics/token-cost-flamegraph-widget.js +954 -0
- package/out/scripts/analytics/token-cost-trend-widget.js +1062 -0
- package/out/scripts/analytics/workflows-collector.cjs +315 -0
- package/out/scripts/analytics/workflows-daemon.mjs +186 -0
- package/out/scripts/analytics/workflows-widget.cjs +433 -0
- package/out/scripts/analytics/wow-model-widget.js +191 -0
- package/out/scripts/gateway/Dockerfile.serve +45 -0
- package/out/scripts/gateway/docker-serve.sh +248 -0
- package/out/scripts/gateway/setup-gateway-server.ps1 +953 -0
- package/out/scripts/gateway/setup-gateway-server.sh +935 -0
- package/out/scripts/gateway/setup-gateway.ps1 +1813 -0
- package/out/scripts/gateway/setup-gateway.sh +1541 -0
- package/out/scripts/setup-cli.ps1 +86 -0
- package/out/scripts/setup-cli.sh +117 -0
- package/out/scripts/tools-manifest.json +83 -0
- package/out/scripts/vendor-tools.mjs +234 -0
- package/out/session.js +272 -0
- package/out/shared/agentEngines.js +170 -0
- package/out/shared/index.js +109 -0
- package/out/shared/models.js +235 -0
- package/out/shared/providers.js +202 -0
- package/out/skills/publish.js +178 -0
- package/out/spotlight/install.js +339 -0
- package/out/spotlight/publish.js +544 -0
- package/out/spotlight/yaml.js +31 -0
- package/out/store/github/setup.js +235 -0
- package/out/store/github/templates/build-index.ts +339 -0
- package/out/store/github/templates/build-index.yml +97 -0
- package/out/store/github/templates/package.json +11 -0
- package/out/store/index.js +755 -0
- package/out/store/oramaIndex.js +884 -0
- package/out/store/shared.js +241 -0
- package/out/sync.js +1827 -0
- package/out/terminalLogger.js +358 -0
- package/out/test/agentEnvFile.test.js +97 -0
- package/out/test/agentYaml.test.js +373 -0
- package/out/test/agentpack.test.js +639 -0
- package/out/test/analytics-lifecycle.test.js +174 -0
- package/out/test/analytics.test.js +524 -0
- package/out/test/auth-bootstrapApply.test.js +102 -0
- package/out/test/auth-broker.test.js +434 -0
- package/out/test/auth-callback.test.js +62 -0
- package/out/test/branding.test.js +229 -0
- package/out/test/cli-agent-id.test.js +55 -0
- package/out/test/credential-context.test.js +105 -0
- package/out/test/customEngineInstall.test.js +100 -0
- package/out/test/customRegistry.test.js +122 -0
- package/out/test/deploy-init.test.js +532 -0
- package/out/test/deps-auto-install.test.js +81 -0
- package/out/test/engine-integration.test.js +297 -0
- package/out/test/enginePaths.test.js +195 -0
- package/out/test/enginePublish.test.js +82 -0
- package/out/test/fleetDisplayUtils.test.js +96 -0
- package/out/test/gateway-client.test.js +381 -0
- package/out/test/gateway-lifecycle.test.js +608 -0
- package/out/test/gateway-outbound.test.js +531 -0
- package/out/test/gateway-reset-mappings.test.js +221 -0
- package/out/test/gateway-route-index.test.js +235 -0
- package/out/test/gateway-session-routing.test.js +47 -0
- package/out/test/gateway-session-store.test.js +216 -0
- package/out/test/github.test.js +232 -0
- package/out/test/headless-sync.test.js +279 -0
- package/out/test/helpers/manifest-fixtures.js +27 -0
- package/out/test/mcp-obo.test.js +82 -0
- package/out/test/mcp.test.js +307 -0
- package/out/test/openclaw-channel-routing.test.js +191 -0
- package/out/test/pathSafety.test.js +63 -0
- package/out/test/permissions.test.js +175 -0
- package/out/test/persistent-engine-worker.test.js +220 -0
- package/out/test/refreshPolicy.test.js +60 -0
- package/out/test/registry.test.js +351 -0
- package/out/test/runtime.test.js +349 -0
- package/out/test/scheduledFleetStorage.test.js +72 -0
- package/out/test/serve-hardening.test.js +192 -0
- package/out/test/serveClient.test.js +173 -0
- package/out/test/sessionJsonlFleet.test.js +78 -0
- package/out/test/shared.test.js +173 -0
- package/out/test/skills-mcp.test.js +299 -0
- package/out/test/spotlightPublish.test.js +299 -0
- package/out/test/spotlightYaml.test.js +58 -0
- package/out/test/state-sync-peer.test.js +74 -0
- package/out/test/store.test.js +331 -0
- package/out/test/stores.test.js +504 -0
- package/out/test/sync-switch.test.js +251 -0
- package/out/test/teamViewDiscovery.test.js +90 -0
- package/out/test/toolPath.test.js +120 -0
- package/out/test/utils.test.js +308 -0
- package/out/test/viewsConfigMerge.test.js +180 -0
- package/out/test/windows-command-line.test.js +75 -0
- package/out/utils/agentEnvFile.js +131 -0
- package/out/utils/binary.js +175 -0
- package/out/utils/index.js +37 -0
- package/out/utils/network.js +74 -0
- package/out/utils/pathSafety.js +60 -0
- package/out/utils/paths.js +116 -0
- package/out/utils/pid.js +180 -0
- package/out/utils/pythonRunner.js +258 -0
- package/out/utils/resolveNodeBinary.js +168 -0
- package/out/utils/toolPath.js +83 -0
- package/out/utils/windowsCommandLine.js +29 -0
- package/out/utils.js +915 -0
- package/out/workspaceFolders.js +59 -0
- 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;
|