@xyagent/cli 0.0.1
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/README.md +860 -0
- package/bin/agentlink +8585 -0
- package/bin/agentlink-agent +7 -0
- package/bin/agentlink-mcp-stdio +173 -0
- package/docs/LOGGING.md +159 -0
- package/package.json +38 -0
- package/src/core.mjs +320 -0
- package/src/tunnel.mjs +291 -0
- package/src/tunnel_proxy.mjs +913 -0
- package/src/tunnel_service.mjs +699 -0
- package/src/tunnel_state.mjs +301 -0
- package/src/unified_producer.mjs +259 -0
- package/src-ext/README.md +75 -0
- package/src-ext/bin.mjs +128 -0
- package/src-ext/commands/agent.mjs +66 -0
- package/src-ext/commands/help.mjs +31 -0
- package/src-ext/commands/pair.mjs +455 -0
- package/src-ext/commands/reload.mjs +78 -0
- package/src-ext/commands/service.mjs +64 -0
- package/src-ext/core/accountResolver.mjs +137 -0
- package/src-ext/core/activeRuns.mjs +76 -0
- package/src-ext/core/agentlinkToolsBootstrap.mjs +75 -0
- package/src-ext/core/agentlinkToolsRegistry.mjs +323 -0
- package/src-ext/core/agentlinkToolsServer.mjs +109 -0
- package/src-ext/core/approvalGateway.mjs +132 -0
- package/src-ext/core/attachAuditLog.mjs +79 -0
- package/src-ext/core/attachDenylist.mjs +128 -0
- package/src-ext/core/autoTunnelDetector.mjs +98 -0
- package/src-ext/core/autoTunnelGateway.mjs +393 -0
- package/src-ext/core/autoTunnelWiring.mjs +184 -0
- package/src-ext/core/boundedMap.mjs +98 -0
- package/src-ext/core/bridgeAttachAutoScanner.mjs +237 -0
- package/src-ext/core/bridgeHooks.mjs +168 -0
- package/src-ext/core/bridgeSelfUninstall.mjs +133 -0
- package/src-ext/core/claudeTrust.mjs +74 -0
- package/src-ext/core/codexTrust.mjs +77 -0
- package/src-ext/core/cosUploadClient.mjs +326 -0
- package/src-ext/core/createWorkspace.mjs +170 -0
- package/src-ext/core/daemonRestart.mjs +49 -0
- package/src-ext/core/defaultWorkspace.mjs +65 -0
- package/src-ext/core/ensureGitRepo.mjs +57 -0
- package/src-ext/core/hermesProfile.mjs +75 -0
- package/src-ext/core/interruptGateway.mjs +119 -0
- package/src-ext/core/interruptRunListener.mjs +106 -0
- package/src-ext/core/mcpRuntimeFanout.mjs +236 -0
- package/src-ext/core/mcpStdioClient.mjs +316 -0
- package/src-ext/core/openclawGatewayClient.mjs +230 -0
- package/src-ext/core/pairCodeClient.mjs +331 -0
- package/src-ext/core/pairInventory.mjs +59 -0
- package/src-ext/core/pathExpand.mjs +34 -0
- package/src-ext/core/preprocessAttachments.mjs +113 -0
- package/src-ext/core/relayWorker.mjs +284 -0
- package/src-ext/core/relayWorkerHttp.mjs +58 -0
- package/src-ext/core/scanWorkspaces.mjs +842 -0
- package/src-ext/core/sessionSlot.mjs +70 -0
- package/src-ext/core/stateMachine.mjs +85 -0
- package/src-ext/core/toolsServerLifecycle.mjs +169 -0
- package/src-ext/core/unifiedDispatchHandler.mjs +439 -0
- package/src-ext/core/usageReporter.mjs +207 -0
- package/src-ext/openclaw-plugin/envelope-builder.cjs +619 -0
- package/src-ext/openclaw-plugin/index.cjs +1919 -0
- package/src-ext/openclaw-plugin/openclaw.plugin.json +20 -0
- package/src-ext/openclaw-plugin/package.json +12 -0
- package/src-ext/openclaw-plugin/relay-defaults.json +3 -0
- package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +101 -0
- package/src-ext/runtime/_shared/cliExec.mjs +138 -0
- package/src-ext/runtime/_shared/cosTextRefs.mjs +120 -0
- package/src-ext/runtime/_shared/fetchAndBase64Encode.mjs +52 -0
- package/src-ext/runtime/_shared/relayObjectToBlock.mjs +307 -0
- package/src-ext/runtime/_shared/sessionPicker.mjs +418 -0
- package/src-ext/runtime/_shared/slashCommandRouter.mjs +1346 -0
- package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +94 -0
- package/src-ext/runtime/claude/askUserQuestionTranslator.mjs +241 -0
- package/src-ext/runtime/claude/customCommandsCollector.mjs +132 -0
- package/src-ext/runtime/claude/handleRequest.mjs +1418 -0
- package/src-ext/runtime/claude/index.mjs +160 -0
- package/src-ext/runtime/claude/internalCommandNames.mjs +58 -0
- package/src-ext/runtime/claude/launcher.mjs +313 -0
- package/src-ext/runtime/claude/mcpConfigAdapter.mjs +174 -0
- package/src-ext/runtime/claude/mcpConfigWriter.mjs +57 -0
- package/src-ext/runtime/claude/modelContextWindow.mjs +30 -0
- package/src-ext/runtime/claude/modelScanner.mjs +257 -0
- package/src-ext/runtime/claude/permissionInterruptBlock.mjs +86 -0
- package/src-ext/runtime/claude/permissionPromptServer.mjs +170 -0
- package/src-ext/runtime/claude/permissionPromptServerWorker.mjs +214 -0
- package/src-ext/runtime/claude/preflight.mjs +62 -0
- package/src-ext/runtime/claude/stdoutParser.mjs +365 -0
- package/src-ext/runtime/claude/todoTranslator.mjs +66 -0
- package/src-ext/runtime/claude/usageProbe.mjs +576 -0
- package/src-ext/runtime/claude/usageProbeStrip.mjs +14 -0
- package/src-ext/runtime/codex/codexCustomCommandsCollector.mjs +87 -0
- package/src-ext/runtime/codex/elicitationHandler.mjs +91 -0
- package/src-ext/runtime/codex/eventMapper.mjs +154 -0
- package/src-ext/runtime/codex/handleRequest.mjs +1152 -0
- package/src-ext/runtime/codex/index.mjs +142 -0
- package/src-ext/runtime/codex/mcpConfigAdapter.mjs +155 -0
- package/src-ext/runtime/codex/modelScanner.mjs +305 -0
- package/src-ext/runtime/codex/planNotificationHandler.mjs +61 -0
- package/src-ext/runtime/codex/preflight.mjs +67 -0
- package/src-ext/runtime/hermes/commandBackend.mjs +117 -0
- package/src-ext/runtime/hermes/envSetup.mjs +229 -0
- package/src-ext/runtime/hermes/gatewayManager.mjs +625 -0
- package/src-ext/runtime/hermes/handleRequest.mjs +1257 -0
- package/src-ext/runtime/hermes/httpBackend.mjs +219 -0
- package/src-ext/runtime/hermes/index.mjs +196 -0
- package/src-ext/runtime/hermes/mcpConfigAdapter.mjs +189 -0
- package/src-ext/runtime/hermes/preflight.mjs +359 -0
- package/src-ext/runtime/hermes/responsesBackend.mjs +276 -0
- package/src-ext/runtime/hermes/sessionStore.mjs +78 -0
- package/src-ext/runtime/hermes/todoTranslator.mjs +90 -0
- package/src-ext/runtime/hermes/workspaceRpc.mjs +124 -0
- package/src-ext/runtime/openclaw/buildOpenclawDaemonInput.mjs +143 -0
- package/src-ext/runtime/openclaw/mcpConfigAdapter.mjs +91 -0
- package/src-ext/runtime/openclaw/openclawConfig.mjs +145 -0
- package/src-ext/runtime/picoclaw/constants.mjs +39 -0
- package/src-ext/runtime/picoclaw/handleRequest.mjs +289 -0
- package/src-ext/runtime/picoclaw/index.mjs +314 -0
- package/src-ext/runtime/picoclaw/pairFlow.mjs +224 -0
- package/src-ext/runtime/picoclaw/state.mjs +78 -0
- package/src-ext/runtime/picoclaw/todoTranslator.mjs +67 -0
- package/src-ext/runtime/picoclaw/translator.mjs +272 -0
- package/src-ext/runtime/picoclaw/wsClient.mjs +129 -0
- package/src-ext/service/serviceManager.mjs +521 -0
- package/src-shared/README.md +44 -0
- package/src-shared/chat_push_normalizer.mjs +113 -0
- package/src-shared/correlation.mjs +25 -0
- package/src-shared/envelope_builder.mjs +612 -0
- package/src-shared/logger.mjs +167 -0
- package/src-shared/relay-defaults.json +3 -0
- package/src-shared/relay_revoke.mjs +46 -0
- package/src-shared/vendor/README.md +60 -0
- package/src-shared/vendor/smol-toml/LICENSE +24 -0
- package/src-shared/vendor/smol-toml/README.md +236 -0
- package/src-shared/vendor/smol-toml/VENDORED.md +37 -0
- package/src-shared/vendor/smol-toml/dist/date.d.ts +41 -0
- package/src-shared/vendor/smol-toml/dist/date.js +127 -0
- package/src-shared/vendor/smol-toml/dist/error.d.ts +38 -0
- package/src-shared/vendor/smol-toml/dist/error.js +63 -0
- package/src-shared/vendor/smol-toml/dist/extract.d.ts +30 -0
- package/src-shared/vendor/smol-toml/dist/extract.js +109 -0
- package/src-shared/vendor/smol-toml/dist/index.cjs +900 -0
- package/src-shared/vendor/smol-toml/dist/index.d.ts +43 -0
- package/src-shared/vendor/smol-toml/dist/index.js +33 -0
- package/src-shared/vendor/smol-toml/dist/parse.d.ts +37 -0
- package/src-shared/vendor/smol-toml/dist/parse.js +148 -0
- package/src-shared/vendor/smol-toml/dist/primitive.d.ts +31 -0
- package/src-shared/vendor/smol-toml/dist/primitive.js +178 -0
- package/src-shared/vendor/smol-toml/dist/stringify.d.ts +31 -0
- package/src-shared/vendor/smol-toml/dist/stringify.js +163 -0
- package/src-shared/vendor/smol-toml/dist/struct.d.ts +32 -0
- package/src-shared/vendor/smol-toml/dist/struct.js +194 -0
- package/src-shared/vendor/smol-toml/dist/util.d.ts +42 -0
- package/src-shared/vendor/smol-toml/dist/util.js +100 -0
- package/src-shared/vendor/smol-toml/package.json +54 -0
- package/src-shared/vendor/yaml/LICENSE +13 -0
- package/src-shared/vendor/yaml/README.md +155 -0
- package/src-shared/vendor/yaml/VENDORED.md +29 -0
- package/src-shared/vendor/yaml/bin.mjs +11 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-collection.js +88 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-doc.js +42 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-node.js +92 -0
- package/src-shared/vendor/yaml/browser/dist/compose/compose-scalar.js +80 -0
- package/src-shared/vendor/yaml/browser/dist/compose/composer.js +217 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-map.js +113 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-seq.js +47 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-end.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-collection.js +203 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
- package/src-shared/vendor/yaml/browser/dist/compose/resolve-props.js +148 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-contains-newline.js +34 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-empty-scalar-position.js +27 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
- package/src-shared/vendor/yaml/browser/dist/compose/util-map-includes.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/doc/Document.js +334 -0
- package/src-shared/vendor/yaml/browser/dist/doc/anchors.js +72 -0
- package/src-shared/vendor/yaml/browser/dist/doc/applyReviver.js +55 -0
- package/src-shared/vendor/yaml/browser/dist/doc/createNode.js +89 -0
- package/src-shared/vendor/yaml/browser/dist/doc/directives.js +176 -0
- package/src-shared/vendor/yaml/browser/dist/errors.js +57 -0
- package/src-shared/vendor/yaml/browser/dist/index.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/log.js +14 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Alias.js +101 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Collection.js +147 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Node.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Pair.js +36 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/Scalar.js +24 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/YAMLMap.js +144 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/addPairToJSMap.js +104 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/identity.js +36 -0
- package/src-shared/vendor/yaml/browser/dist/nodes/toJS.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-scalar.js +214 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-stringify.js +61 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst-visit.js +97 -0
- package/src-shared/vendor/yaml/browser/dist/parse/cst.js +98 -0
- package/src-shared/vendor/yaml/browser/dist/parse/lexer.js +717 -0
- package/src-shared/vendor/yaml/browser/dist/parse/line-counter.js +39 -0
- package/src-shared/vendor/yaml/browser/dist/parse/parser.js +954 -0
- package/src-shared/vendor/yaml/browser/dist/public-api.js +99 -0
- package/src-shared/vendor/yaml/browser/dist/schema/Schema.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/map.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/null.js +15 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/seq.js +17 -0
- package/src-shared/vendor/yaml/browser/dist/schema/common/string.js +14 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/bool.js +19 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/float.js +43 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/int.js +38 -0
- package/src-shared/vendor/yaml/browser/dist/schema/core/schema.js +23 -0
- package/src-shared/vendor/yaml/browser/dist/schema/json/schema.js +62 -0
- package/src-shared/vendor/yaml/browser/dist/schema/tags.js +83 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/binary.js +66 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/schema.js +37 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
- package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringify.js +124 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyCollection.js +143 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyComment.js +20 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyPair.js +150 -0
- package/src-shared/vendor/yaml/browser/dist/stringify/stringifyString.js +328 -0
- package/src-shared/vendor/yaml/browser/dist/util.js +11 -0
- package/src-shared/vendor/yaml/browser/dist/visit.js +233 -0
- package/src-shared/vendor/yaml/browser/index.js +5 -0
- package/src-shared/vendor/yaml/browser/package.json +3 -0
- package/src-shared/vendor/yaml/dist/cli.d.ts +8 -0
- package/src-shared/vendor/yaml/dist/cli.mjs +199 -0
- package/src-shared/vendor/yaml/dist/compose/compose-collection.d.ts +11 -0
- package/src-shared/vendor/yaml/dist/compose/compose-collection.js +90 -0
- package/src-shared/vendor/yaml/dist/compose/compose-doc.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/compose/compose-doc.js +44 -0
- package/src-shared/vendor/yaml/dist/compose/compose-node.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/compose/compose-node.js +95 -0
- package/src-shared/vendor/yaml/dist/compose/compose-scalar.d.ts +5 -0
- package/src-shared/vendor/yaml/dist/compose/compose-scalar.js +82 -0
- package/src-shared/vendor/yaml/dist/compose/composer.d.ts +62 -0
- package/src-shared/vendor/yaml/dist/compose/composer.js +221 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-map.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.js +200 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-end.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-end.js +39 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.js +205 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-props.d.ts +23 -0
- package/src-shared/vendor/yaml/dist/compose/resolve-props.js +150 -0
- package/src-shared/vendor/yaml/dist/compose/util-contains-newline.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/compose/util-contains-newline.js +36 -0
- package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.js +29 -0
- package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.js +17 -0
- package/src-shared/vendor/yaml/dist/compose/util-map-includes.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/compose/util-map-includes.js +19 -0
- package/src-shared/vendor/yaml/dist/doc/Document.d.ts +141 -0
- package/src-shared/vendor/yaml/dist/doc/Document.js +336 -0
- package/src-shared/vendor/yaml/dist/doc/anchors.d.ts +24 -0
- package/src-shared/vendor/yaml/dist/doc/anchors.js +77 -0
- package/src-shared/vendor/yaml/dist/doc/applyReviver.d.ts +9 -0
- package/src-shared/vendor/yaml/dist/doc/applyReviver.js +57 -0
- package/src-shared/vendor/yaml/dist/doc/createNode.d.ts +17 -0
- package/src-shared/vendor/yaml/dist/doc/createNode.js +91 -0
- package/src-shared/vendor/yaml/dist/doc/directives.d.ts +49 -0
- package/src-shared/vendor/yaml/dist/doc/directives.js +178 -0
- package/src-shared/vendor/yaml/dist/errors.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/errors.js +62 -0
- package/src-shared/vendor/yaml/dist/index.d.ts +22 -0
- package/src-shared/vendor/yaml/dist/index.js +50 -0
- package/src-shared/vendor/yaml/dist/log.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/log.js +17 -0
- package/src-shared/vendor/yaml/dist/nodes/Alias.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/nodes/Alias.js +103 -0
- package/src-shared/vendor/yaml/dist/nodes/Collection.d.ts +73 -0
- package/src-shared/vendor/yaml/dist/nodes/Collection.js +151 -0
- package/src-shared/vendor/yaml/dist/nodes/Node.d.ts +47 -0
- package/src-shared/vendor/yaml/dist/nodes/Node.js +40 -0
- package/src-shared/vendor/yaml/dist/nodes/Pair.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/nodes/Pair.js +39 -0
- package/src-shared/vendor/yaml/dist/nodes/Scalar.d.ts +42 -0
- package/src-shared/vendor/yaml/dist/nodes/Scalar.js +27 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLMap.d.ts +53 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLMap.js +147 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
- package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.js +115 -0
- package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.js +106 -0
- package/src-shared/vendor/yaml/dist/nodes/identity.d.ts +23 -0
- package/src-shared/vendor/yaml/dist/nodes/identity.js +53 -0
- package/src-shared/vendor/yaml/dist/nodes/toJS.d.ts +27 -0
- package/src-shared/vendor/yaml/dist/nodes/toJS.js +39 -0
- package/src-shared/vendor/yaml/dist/options.d.ts +338 -0
- package/src-shared/vendor/yaml/dist/parse/cst-scalar.d.ts +64 -0
- package/src-shared/vendor/yaml/dist/parse/cst-scalar.js +218 -0
- package/src-shared/vendor/yaml/dist/parse/cst-stringify.d.ts +8 -0
- package/src-shared/vendor/yaml/dist/parse/cst-stringify.js +63 -0
- package/src-shared/vendor/yaml/dist/parse/cst-visit.d.ts +39 -0
- package/src-shared/vendor/yaml/dist/parse/cst-visit.js +99 -0
- package/src-shared/vendor/yaml/dist/parse/cst.d.ts +108 -0
- package/src-shared/vendor/yaml/dist/parse/cst.js +112 -0
- package/src-shared/vendor/yaml/dist/parse/lexer.d.ts +87 -0
- package/src-shared/vendor/yaml/dist/parse/lexer.js +719 -0
- package/src-shared/vendor/yaml/dist/parse/line-counter.d.ts +22 -0
- package/src-shared/vendor/yaml/dist/parse/line-counter.js +41 -0
- package/src-shared/vendor/yaml/dist/parse/parser.d.ts +84 -0
- package/src-shared/vendor/yaml/dist/parse/parser.js +958 -0
- package/src-shared/vendor/yaml/dist/public-api.d.ts +43 -0
- package/src-shared/vendor/yaml/dist/public-api.js +104 -0
- package/src-shared/vendor/yaml/dist/schema/Schema.d.ts +18 -0
- package/src-shared/vendor/yaml/dist/schema/Schema.js +40 -0
- package/src-shared/vendor/yaml/dist/schema/common/map.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/map.js +19 -0
- package/src-shared/vendor/yaml/dist/schema/common/null.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/common/null.js +17 -0
- package/src-shared/vendor/yaml/dist/schema/common/seq.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/seq.js +19 -0
- package/src-shared/vendor/yaml/dist/schema/common/string.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/common/string.js +16 -0
- package/src-shared/vendor/yaml/dist/schema/core/bool.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/bool.js +21 -0
- package/src-shared/vendor/yaml/dist/schema/core/float.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/float.js +47 -0
- package/src-shared/vendor/yaml/dist/schema/core/int.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/core/int.js +42 -0
- package/src-shared/vendor/yaml/dist/schema/core/schema.d.ts +1 -0
- package/src-shared/vendor/yaml/dist/schema/core/schema.js +25 -0
- package/src-shared/vendor/yaml/dist/schema/json/schema.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/json/schema.js +64 -0
- package/src-shared/vendor/yaml/dist/schema/json-schema.d.ts +69 -0
- package/src-shared/vendor/yaml/dist/schema/tags.d.ts +40 -0
- package/src-shared/vendor/yaml/dist/schema/tags.js +86 -0
- package/src-shared/vendor/yaml/dist/schema/types.d.ts +90 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.js +68 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.js +29 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.js +50 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.js +76 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.js +77 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.js +96 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
- package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
- package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
- package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.js +151 -0
- package/src-shared/vendor/yaml/dist/stringify/stringify.d.ts +21 -0
- package/src-shared/vendor/yaml/dist/stringify/stringify.js +127 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.js +145 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyComment.d.ts +10 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyComment.js +24 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.js +87 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.js +26 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyPair.d.ts +3 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyPair.js +152 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyString.d.ts +9 -0
- package/src-shared/vendor/yaml/dist/stringify/stringifyString.js +330 -0
- package/src-shared/vendor/yaml/dist/test-events.d.ts +4 -0
- package/src-shared/vendor/yaml/dist/test-events.js +134 -0
- package/src-shared/vendor/yaml/dist/util.d.ts +12 -0
- package/src-shared/vendor/yaml/dist/util.js +28 -0
- package/src-shared/vendor/yaml/dist/visit.d.ts +102 -0
- package/src-shared/vendor/yaml/dist/visit.js +236 -0
- package/src-shared/vendor/yaml/package.json +96 -0
- package/src-shared/vendor/yaml/util.js +2 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// hermes-agent gateway 进程管理 —— 给 `agentlink pair -r hermes` 用:
|
|
2
|
+
// 配置改完之后直接 kill + spawn 让 gateway 自动起来,**用户不用再手敲
|
|
3
|
+
// nohup**。
|
|
4
|
+
//
|
|
5
|
+
// 设计要点:
|
|
6
|
+
// - 不接管常驻管理 —— 用户已经用 launchctl/systemd-user 托管的话,我们
|
|
7
|
+
// 探测到 :8642 通就什么也不做;不通才介入。
|
|
8
|
+
// - spawnDetached + unref → 子进程脱离 pair 进程的生命周期,pair 退出后
|
|
9
|
+
// gateway 继续跑。
|
|
10
|
+
// - 不靠 pkill —— 用 lsof 找端口占用者拿 pid 精准 kill,避免误杀其它叫
|
|
11
|
+
// "hermes gateway"的进程。
|
|
12
|
+
|
|
13
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import net from "node:net";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
// gateway 启动后 listen 端口需要时间 —— 热路径 1.5~3s,但冷启动(首次加载
|
|
20
|
+
// skills/记忆/模型路由)实测可 >8s;给到 30s 上限,避免误判超时后回退到默认 profile(错身份)。
|
|
21
|
+
const HEALTH_POLL_INTERVAL_MS = 300;
|
|
22
|
+
const HEALTH_POLL_TIMEOUT_MS = 30000;
|
|
23
|
+
// kill 之后等端口完全释放,避免 spawn 时撞 EADDRINUSE。
|
|
24
|
+
const PORT_RELEASE_TIMEOUT_MS = 5000;
|
|
25
|
+
const PORT_RELEASE_INTERVAL_MS = 200;
|
|
26
|
+
|
|
27
|
+
// per-profile 端口范围(default profile 固定 8642,不在此范围内)
|
|
28
|
+
const PROFILE_PORT_MIN = 8643;
|
|
29
|
+
const PROFILE_PORT_MAX = 8742;
|
|
30
|
+
|
|
31
|
+
// 进程级 profile gateway 注册表
|
|
32
|
+
// Map<slug, { port: number, pid: number, baseUrl: string, lastActiveAt: Date }>
|
|
33
|
+
export const profileGatewayRegistry = new Map();
|
|
34
|
+
|
|
35
|
+
// 最大并发 profile gateway 数(含 default profile)
|
|
36
|
+
export const MAX_PROFILE_GATEWAYS = Number(
|
|
37
|
+
process.env.HERMES_MAX_PROFILE_GATEWAYS || "5",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// idle 回收阈值(分钟)
|
|
41
|
+
const IDLE_TIMEOUT_MINUTES = Number(
|
|
42
|
+
process.env.HERMES_GATEWAY_IDLE_TIMEOUT_MINUTES || "30",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// idle 回收 timer(每 5 分钟扫一次)
|
|
46
|
+
let _idleCleanupTimer = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 手动触发 idle 回收(也供测试调用)。
|
|
50
|
+
*/
|
|
51
|
+
export async function runIdleCleanup({ log } = {}) {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const thresholdMs = IDLE_TIMEOUT_MINUTES * 60 * 1000;
|
|
54
|
+
for (const [slug, info] of profileGatewayRegistry.entries()) {
|
|
55
|
+
const idleMs = now - info.lastActiveAt.getTime();
|
|
56
|
+
if (idleMs >= thresholdMs) {
|
|
57
|
+
log?.info?.("hermes.profile.idle_evict", "evicting idle profile gateway", {
|
|
58
|
+
slug, idle_minutes: Math.floor(idleMs / 60000), port: info.port,
|
|
59
|
+
});
|
|
60
|
+
await stopGatewayOnPort(info.port, { log }).catch(() => {});
|
|
61
|
+
profileGatewayRegistry.delete(slug);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 启动进程级 idle 回收后台 timer(只启动一次)。
|
|
68
|
+
*/
|
|
69
|
+
export function startIdleCleanupTimer({ log } = {}) {
|
|
70
|
+
if (_idleCleanupTimer) return;
|
|
71
|
+
_idleCleanupTimer = setInterval(() => {
|
|
72
|
+
runIdleCleanup({ log }).catch(() => {});
|
|
73
|
+
}, 5 * 60 * 1000);
|
|
74
|
+
// 不阻止进程退出
|
|
75
|
+
if (_idleCleanupTimer.unref) _idleCleanupTimer.unref();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 检查端口是否空闲(返回 Promise<boolean>)。
|
|
80
|
+
* 通过 net.createServer().listen() 探测,比 lsof 更准确(跨平台)。
|
|
81
|
+
*/
|
|
82
|
+
function isPortFree(port) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const server = net.createServer();
|
|
85
|
+
server.once("error", () => resolve(false));
|
|
86
|
+
server.once("listening", () => {
|
|
87
|
+
server.close(() => resolve(true));
|
|
88
|
+
});
|
|
89
|
+
server.listen(port, "127.0.0.1");
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 为 per-profile hermesHome 分配/复用端口。
|
|
95
|
+
*
|
|
96
|
+
* 规则:
|
|
97
|
+
* 1. hermesHome == ~/.hermes → 固定返回 8642(向后兼容)
|
|
98
|
+
* 2. 读 <hermesHome>/gateway-port.json,若端口空闲 → 复用
|
|
99
|
+
* 3. 否则在 [8643, 8742] 探测空闲端口,写入文件
|
|
100
|
+
* 4. 全满 → 抛出错误
|
|
101
|
+
*
|
|
102
|
+
* @param {string} hermesHome
|
|
103
|
+
* @returns {Promise<number>}
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 读 `<hermesHome>/.env` 里某个 key 的值(去包裹引号/首尾空白)。
|
|
108
|
+
* 文件不存在 / 读失败 / 未命中 → 返回 ""。
|
|
109
|
+
* @param {string} hermesHome
|
|
110
|
+
* @param {string} key
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function readProfileEnvValue(hermesHome, key) {
|
|
114
|
+
try {
|
|
115
|
+
const raw = fs.readFileSync(path.join(hermesHome, ".env"), "utf8");
|
|
116
|
+
for (const line of raw.split("\n")) {
|
|
117
|
+
const t = line.trim();
|
|
118
|
+
if (!t || t.startsWith("#")) continue;
|
|
119
|
+
const eq = t.indexOf("=");
|
|
120
|
+
if (eq < 0) continue;
|
|
121
|
+
if (t.slice(0, eq).trim() !== key) continue;
|
|
122
|
+
let v = t.slice(eq + 1).trim();
|
|
123
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
124
|
+
v = v.slice(1, -1);
|
|
125
|
+
}
|
|
126
|
+
return v;
|
|
127
|
+
}
|
|
128
|
+
} catch { /* .env 不存在或读失败 */ }
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function resolveProfilePort(hermesHome) {
|
|
133
|
+
// default profile → 固定 8642
|
|
134
|
+
const defaultHome = path.join(os.homedir(), ".hermes");
|
|
135
|
+
if (path.normalize(hermesHome) === path.normalize(defaultHome)) {
|
|
136
|
+
return 8642;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 优先用 profile 自己 .env 的 API_SERVER_PORT —— hermes gateway 启动时按它绑端口,
|
|
140
|
+
// bridge 期望端口必须与之一致,否则会路由到别的 profile 的 gateway(跨 profile 串台 bug)。
|
|
141
|
+
const envPort = Number(readProfileEnvValue(hermesHome, "API_SERVER_PORT"));
|
|
142
|
+
if (Number.isFinite(envPort) && envPort > 0) {
|
|
143
|
+
return envPort;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const portFile = path.join(hermesHome, "gateway-port.json");
|
|
147
|
+
|
|
148
|
+
// 尝试读已持久化的端口
|
|
149
|
+
try {
|
|
150
|
+
const raw = fs.readFileSync(portFile, "utf8");
|
|
151
|
+
const saved = JSON.parse(raw);
|
|
152
|
+
const savedPort = Number(saved?.port);
|
|
153
|
+
if (Number.isFinite(savedPort) && savedPort > 0) {
|
|
154
|
+
const free = await isPortFree(savedPort);
|
|
155
|
+
if (free) return savedPort;
|
|
156
|
+
// 端口被占——可能是我们的 gateway,也可能是无关进程。
|
|
157
|
+
// 按 spec: 端口被占 → 重探新端口(调用侧会在 spawn 前 stop 掉旧 pid)
|
|
158
|
+
}
|
|
159
|
+
} catch { /* 文件不存在或解析失败,继续探测 */ }
|
|
160
|
+
|
|
161
|
+
// 顺序探测 [8643, 8742]
|
|
162
|
+
for (let port = PROFILE_PORT_MIN; port <= PROFILE_PORT_MAX; port++) {
|
|
163
|
+
const free = await isPortFree(port);
|
|
164
|
+
if (free) {
|
|
165
|
+
// 持久化
|
|
166
|
+
try {
|
|
167
|
+
fs.mkdirSync(hermesHome, { recursive: true });
|
|
168
|
+
fs.writeFileSync(portFile, JSON.stringify({ port }), "utf8");
|
|
169
|
+
} catch { /* 写失败不影响返回 */ }
|
|
170
|
+
return port;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new Error(
|
|
175
|
+
`no available port in range ${PROFILE_PORT_MIN}-${PROFILE_PORT_MAX} for hermes profile gateway`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* per-profile gateway 生命周期入口。
|
|
181
|
+
*
|
|
182
|
+
* @param {string} slug profile 名(gwId.toLowerCase().slice(0,32))
|
|
183
|
+
* @param {string} hermesHome profile 目录绝对路径
|
|
184
|
+
* @param {{ mode?: "ensure"|"restart", log?: any, binary?: string }} opts
|
|
185
|
+
* @returns {Promise<{ ok: boolean, started: boolean, pid?: number, port: number, baseUrl: string, logPath: string, error?: string }>}
|
|
186
|
+
*/
|
|
187
|
+
export async function manageProfileGateway(slug, hermesHome, {
|
|
188
|
+
mode = "ensure", log, binary,
|
|
189
|
+
} = {}) {
|
|
190
|
+
const eLog = log?.child ? log.child({ comp: "hermes.profile.gateway", slug }) : log;
|
|
191
|
+
|
|
192
|
+
const logPath = path.join(hermesHome, "gateway.log");
|
|
193
|
+
const pidFile = path.join(hermesHome, "gateway.pid");
|
|
194
|
+
|
|
195
|
+
// mode=ensure:先探活已有 gateway(用注册表或 port file 里的端口),健康则 no-op
|
|
196
|
+
// 这样避免 resolveProfilePort 因 gateway 在运行而错误地重分配端口
|
|
197
|
+
if (mode === "ensure") {
|
|
198
|
+
// 先查内存注册表
|
|
199
|
+
const cached = profileGatewayRegistry.get(slug);
|
|
200
|
+
if (cached && cached.port) {
|
|
201
|
+
const cachedBase = `http://127.0.0.1:${cached.port}`;
|
|
202
|
+
const probe = await probeHealth(cachedBase, { timeoutMs: 1500 });
|
|
203
|
+
if (probe.ok) {
|
|
204
|
+
cached.lastActiveAt = new Date();
|
|
205
|
+
eLog?.info?.("hermes.profile.gateway.ensure.cached_ok", "profile gateway healthy (cached)", { slug, port: cached.port });
|
|
206
|
+
return { ok: true, started: false, pid: cached.pid, port: cached.port, baseUrl: cachedBase, logPath };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 尝试读 port file 里的端口做 health check(gateway 可能在运行但不在注册表里)
|
|
210
|
+
const portFileRead = (() => {
|
|
211
|
+
try {
|
|
212
|
+
const raw = fs.readFileSync(path.join(hermesHome, "gateway-port.json"), "utf8");
|
|
213
|
+
const saved = JSON.parse(raw);
|
|
214
|
+
const p = Number(saved?.port);
|
|
215
|
+
return (Number.isFinite(p) && p > 0) ? p : null;
|
|
216
|
+
} catch { return null; }
|
|
217
|
+
})();
|
|
218
|
+
if (portFileRead) {
|
|
219
|
+
const savedBase = `http://127.0.0.1:${portFileRead}`;
|
|
220
|
+
const probe = await probeHealth(savedBase, { timeoutMs: 1500 });
|
|
221
|
+
if (probe.ok) {
|
|
222
|
+
eLog?.info?.("hermes.profile.gateway.ensure.portfile_ok", "profile gateway healthy (port file)", { slug, port: portFileRead });
|
|
223
|
+
profileGatewayRegistry.set(slug, {
|
|
224
|
+
port: portFileRead, pid: 0, baseUrl: savedBase, lastActiveAt: new Date(),
|
|
225
|
+
});
|
|
226
|
+
return { ok: true, started: false, pid: 0, port: portFileRead, baseUrl: savedBase, logPath };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 需要 spawn:调 resolveProfilePort 分配(或重新分配)端口
|
|
232
|
+
let port;
|
|
233
|
+
try {
|
|
234
|
+
port = await resolveProfilePort(hermesHome);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return { ok: false, started: false, port: 0, baseUrl: "", logPath,
|
|
237
|
+
error: err?.message || String(err) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
241
|
+
|
|
242
|
+
// 需要 spawn:先检查并发上限,执行 LRU 驱逐
|
|
243
|
+
await _evictIfNeeded({ eLog });
|
|
244
|
+
|
|
245
|
+
const effectiveBinary = binary ?? findHermesBinary();
|
|
246
|
+
if (!effectiveBinary) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false, started: false, port, baseUrl, logPath,
|
|
249
|
+
error: "hermes 可执行文件没在 PATH 找到(也试了 ~/.local/bin / ~/.hermes/.../venv/bin / /usr/local/bin / /opt/homebrew/bin)。装 hermes-agent 后再 pair。",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// per-profile pid file 路径(不用全局 ~/.hermes/gateway.pid)
|
|
254
|
+
const readProfilePid = () => {
|
|
255
|
+
try {
|
|
256
|
+
const raw = fs.readFileSync(pidFile, "utf8").trim();
|
|
257
|
+
if (!raw) return null;
|
|
258
|
+
let pid;
|
|
259
|
+
if (raw.startsWith("{")) {
|
|
260
|
+
try { pid = JSON.parse(raw)?.pid; } catch { return null; }
|
|
261
|
+
} else {
|
|
262
|
+
pid = parseInt(raw, 10);
|
|
263
|
+
}
|
|
264
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
265
|
+
try { process.kill(pid, 0); return pid; } catch { return null; }
|
|
266
|
+
} catch { return null; }
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// stop 旧 gateway(用 per-profile pid file 或 lsof)
|
|
270
|
+
const pidFromFile = readProfilePid();
|
|
271
|
+
const pidFromPort = findListeningPid(port);
|
|
272
|
+
const oldPid = pidFromFile ?? pidFromPort;
|
|
273
|
+
if (oldPid !== null) {
|
|
274
|
+
eLog?.info?.("hermes.profile.gateway.stop", "stopping old profile gateway", { slug, port, pid: oldPid });
|
|
275
|
+
try { process.kill(oldPid, "SIGTERM"); } catch {}
|
|
276
|
+
await waitForPortFree(port, { timeoutMs: 2500 });
|
|
277
|
+
await waitForPidGone(oldPid, { timeoutMs: 1500 });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// spawn 新 gateway
|
|
281
|
+
eLog?.info?.("hermes.profile.gateway.spawn", "spawning profile gateway", { slug, port, hermesHome });
|
|
282
|
+
let spawnResult;
|
|
283
|
+
try {
|
|
284
|
+
spawnResult = spawnGatewayDetached({
|
|
285
|
+
binary: effectiveBinary,
|
|
286
|
+
logPath,
|
|
287
|
+
log: eLog,
|
|
288
|
+
env: { HERMES_HOME: hermesHome },
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false, started: false, port, baseUrl, logPath,
|
|
293
|
+
error: `profile gateway spawn 失败: ${err?.message || err}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const { pid, logPath: lp } = spawnResult;
|
|
297
|
+
|
|
298
|
+
// 等健康
|
|
299
|
+
const healthResult = await waitForHealth(baseUrl);
|
|
300
|
+
if (!healthResult.ok) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false, started: true, pid, port, baseUrl, logPath: lp,
|
|
303
|
+
error: `profile gateway spawn 后 ${HEALTH_POLL_TIMEOUT_MS}ms 内未通过 /health。看日志:${lp}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 更新注册表
|
|
308
|
+
profileGatewayRegistry.set(slug, {
|
|
309
|
+
port, pid, baseUrl, lastActiveAt: new Date(),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// 启动 idle 回收 timer(第一个 profile gateway spawn 时启动)
|
|
313
|
+
startIdleCleanupTimer({ log });
|
|
314
|
+
|
|
315
|
+
return { ok: true, started: true, pid, port, baseUrl, logPath: lp };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 检查并发上限,必要时 LRU 驱逐最旧 profile。
|
|
320
|
+
*/
|
|
321
|
+
async function _evictIfNeeded({ eLog } = {}) {
|
|
322
|
+
if (profileGatewayRegistry.size < MAX_PROFILE_GATEWAYS) return;
|
|
323
|
+
|
|
324
|
+
// 找 lastActiveAt 最早的
|
|
325
|
+
let oldestSlug = null;
|
|
326
|
+
let oldestTime = Infinity;
|
|
327
|
+
for (const [slug, info] of profileGatewayRegistry.entries()) {
|
|
328
|
+
const t = info.lastActiveAt.getTime();
|
|
329
|
+
if (t < oldestTime) {
|
|
330
|
+
oldestTime = t;
|
|
331
|
+
oldestSlug = slug;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!oldestSlug) return;
|
|
335
|
+
|
|
336
|
+
const info = profileGatewayRegistry.get(oldestSlug);
|
|
337
|
+
eLog?.info?.("hermes.profile.gateway.lru_evict", "LRU evicting profile gateway", {
|
|
338
|
+
slug: oldestSlug, port: info.port, last_active: info.lastActiveAt,
|
|
339
|
+
});
|
|
340
|
+
await stopGatewayOnPort(info.port, { log: eLog }).catch(() => {});
|
|
341
|
+
profileGatewayRegistry.delete(oldestSlug);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 找 hermes 可执行文件。优先 PATH,然后常见 venv 位置。
|
|
346
|
+
* 找不到返回 null(pair 上游打错就行,本模块不退出进程)。
|
|
347
|
+
*/
|
|
348
|
+
export function findHermesBinary() {
|
|
349
|
+
// 1. which hermes
|
|
350
|
+
try {
|
|
351
|
+
const r = spawnSync("which", ["hermes"], { encoding: "utf8" });
|
|
352
|
+
if (r.status === 0) {
|
|
353
|
+
const p = String(r.stdout || "").trim().split("\n")[0];
|
|
354
|
+
if (p && fs.existsSync(p)) return p;
|
|
355
|
+
}
|
|
356
|
+
} catch { /* fall through */ }
|
|
357
|
+
// 2. 用户常见安装位置
|
|
358
|
+
for (const candidate of [
|
|
359
|
+
path.join(os.homedir(), ".local", "bin", "hermes"),
|
|
360
|
+
path.join(os.homedir(), ".hermes", "hermes-agent", "venv", "bin", "hermes"),
|
|
361
|
+
"/usr/local/bin/hermes",
|
|
362
|
+
"/opt/homebrew/bin/hermes",
|
|
363
|
+
]) {
|
|
364
|
+
try {
|
|
365
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
366
|
+
} catch { /* skip */ }
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* lsof 找当前 listen 指定端口的 pid(macOS / Linux 通用)。
|
|
373
|
+
* 找不到(端口空闲)返回 null。
|
|
374
|
+
*/
|
|
375
|
+
export function findListeningPid(port) {
|
|
376
|
+
try {
|
|
377
|
+
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -Fp 2>/dev/null`, {
|
|
378
|
+
encoding: "utf8", timeout: 3000,
|
|
379
|
+
});
|
|
380
|
+
// 输出形如 "p12345\n" — 一行一个 pid 前缀 'p'
|
|
381
|
+
const m = out.match(/^p(\d+)/m);
|
|
382
|
+
return m ? Number(m[1]) : null;
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* 读 hermes 自家 pid file `~/.hermes/gateway.pid`,覆盖「进程在但 API_SERVER
|
|
390
|
+
* 没开 → 没监听 :8642 → lsof 找不到」这种场景。
|
|
391
|
+
*
|
|
392
|
+
* 文件格式是 JSON:`{"pid": 94014, "kind": "hermes-gateway", "argv": [...]}`,
|
|
393
|
+
* 早期版本 hermes 写过纯整数,两种都兼容。返回 null = pid 文件不存在 / 解析
|
|
394
|
+
* 失败 / 进程已死(stale 文件)。
|
|
395
|
+
*/
|
|
396
|
+
export function readHermesGatewayPid() {
|
|
397
|
+
try {
|
|
398
|
+
const pidPath = path.join(os.homedir(), ".hermes", "gateway.pid");
|
|
399
|
+
const raw = fs.readFileSync(pidPath, "utf8").trim();
|
|
400
|
+
if (!raw) return null;
|
|
401
|
+
let pid;
|
|
402
|
+
if (raw.startsWith("{")) {
|
|
403
|
+
try { pid = JSON.parse(raw)?.pid; } catch { return null; }
|
|
404
|
+
} else {
|
|
405
|
+
pid = parseInt(raw, 10);
|
|
406
|
+
}
|
|
407
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
408
|
+
// 验证 pid 还活着 —— signal 0 不真发信号,只检查进程
|
|
409
|
+
try { process.kill(pid, 0); return pid; } catch { return null; }
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 等 pid 真的退出。pid 已经不存在或 process.kill(pid,0) 抛 ESRCH 视为已退。
|
|
417
|
+
*/
|
|
418
|
+
async function waitForPidGone(pid, { timeoutMs = 1500 } = {}) {
|
|
419
|
+
const deadline = Date.now() + timeoutMs;
|
|
420
|
+
while (Date.now() < deadline) {
|
|
421
|
+
try { process.kill(pid, 0); } catch { return true; }
|
|
422
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* GET <baseUrl>/health 一次,timeout 3s。返回 {ok, status, error}。
|
|
429
|
+
*/
|
|
430
|
+
export async function probeHealth(baseUrl, { timeoutMs = 3000 } = {}) {
|
|
431
|
+
try {
|
|
432
|
+
const controller = new AbortController();
|
|
433
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
434
|
+
const res = await fetch(`${baseUrl}/health`, { signal: controller.signal });
|
|
435
|
+
clearTimeout(t);
|
|
436
|
+
if (!res.ok) return { ok: false, status: res.status };
|
|
437
|
+
return { ok: true, status: res.status };
|
|
438
|
+
} catch (err) {
|
|
439
|
+
return {
|
|
440
|
+
ok: false,
|
|
441
|
+
error: err?.name === "AbortError" ? `timeout ${timeoutMs}ms` : (err?.message || String(err)),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 轮询 /health 直到通过或超时。
|
|
448
|
+
*/
|
|
449
|
+
export async function waitForHealth(baseUrl, {
|
|
450
|
+
timeoutMs = HEALTH_POLL_TIMEOUT_MS,
|
|
451
|
+
intervalMs = HEALTH_POLL_INTERVAL_MS,
|
|
452
|
+
} = {}) {
|
|
453
|
+
const deadline = Date.now() + timeoutMs;
|
|
454
|
+
let last = null;
|
|
455
|
+
while (Date.now() < deadline) {
|
|
456
|
+
last = await probeHealth(baseUrl, { timeoutMs: Math.min(2000, intervalMs * 4) });
|
|
457
|
+
if (last.ok) return last;
|
|
458
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
459
|
+
}
|
|
460
|
+
return last || { ok: false, error: "no probe attempted" };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 等端口完全释放(避免 kill 后立刻 spawn 撞 EADDRINUSE)。
|
|
465
|
+
*/
|
|
466
|
+
export async function waitForPortFree(port, {
|
|
467
|
+
timeoutMs = PORT_RELEASE_TIMEOUT_MS,
|
|
468
|
+
intervalMs = PORT_RELEASE_INTERVAL_MS,
|
|
469
|
+
} = {}) {
|
|
470
|
+
const deadline = Date.now() + timeoutMs;
|
|
471
|
+
while (Date.now() < deadline) {
|
|
472
|
+
if (findListeningPid(port) === null) return true;
|
|
473
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
474
|
+
}
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 优雅关停 hermes gateway 进程。
|
|
480
|
+
*
|
|
481
|
+
* 双源 pid 解析:
|
|
482
|
+
* 1. `~/.hermes/gateway.pid` —— 覆盖「进程在跑但 API_SERVER 没开 →
|
|
483
|
+
* lsof 看不到 :8642」的关键场景。这种 case 下不杀掉旧 pid 直接 spawn
|
|
484
|
+
* 会撞 hermes 自家的 lock,新进程立刻 "Gateway already running (PID …)"
|
|
485
|
+
* 退出 → waitForHealth 30s 超时 → bridge 重试 → 用户看到三连"启动失败"。
|
|
486
|
+
* 2. lsof `:port` —— 兜底,处理 pid file 缺失 / stale 的边缘情况。
|
|
487
|
+
*
|
|
488
|
+
* 没找到任何 pid → no-op。
|
|
489
|
+
*/
|
|
490
|
+
export async function stopGatewayOnPort(port, { log } = {}) {
|
|
491
|
+
const pidFromFile = readHermesGatewayPid();
|
|
492
|
+
const pidFromPort = findListeningPid(port);
|
|
493
|
+
// 优先用 pid file —— 它能反映"进程在但没监听端口"这种我们必须处理的状态。
|
|
494
|
+
// 都有但不一致(极少见)时也用 pid file 的,因为 hermes 自己就靠它判断 lock。
|
|
495
|
+
const pid = pidFromFile ?? pidFromPort;
|
|
496
|
+
if (pid === null) {
|
|
497
|
+
log?.info?.("hermes.gateway.stop.noop", "no gateway pid found", { port });
|
|
498
|
+
return { stopped: false, alreadyStopped: true };
|
|
499
|
+
}
|
|
500
|
+
log?.info?.("hermes.gateway.stop", "sending SIGTERM", {
|
|
501
|
+
port, pid, source: pidFromFile != null ? "pid_file" : "port_lsof",
|
|
502
|
+
});
|
|
503
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* dead already */ }
|
|
504
|
+
// 双重等待:端口释放 + 进程退出。hermes 进程可能持有 lock 但不 listen 端口
|
|
505
|
+
// (API_SERVER off 场景),单靠 waitForPortFree 会立刻成功 → 我们以为 stop
|
|
506
|
+
// 完成 → spawn 撞 lock。必须等进程真退出。
|
|
507
|
+
const released = await waitForPortFree(port, { timeoutMs: 2500 });
|
|
508
|
+
const gone = await waitForPidGone(pid, { timeoutMs: 1500 });
|
|
509
|
+
if (released && gone) {
|
|
510
|
+
return { stopped: true, pid, signal: "SIGTERM" };
|
|
511
|
+
}
|
|
512
|
+
log?.warn?.("hermes.gateway.stop.force", "SIGTERM 未完成, 升级 SIGKILL", {
|
|
513
|
+
port, pid, port_released: released, pid_gone: gone,
|
|
514
|
+
});
|
|
515
|
+
try { process.kill(pid, "SIGKILL"); } catch { /* dead already */ }
|
|
516
|
+
await waitForPortFree(port, { timeoutMs: 2500 });
|
|
517
|
+
await waitForPidGone(pid, { timeoutMs: 1500 });
|
|
518
|
+
return { stopped: true, pid, signal: "SIGKILL" };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* spawn detached `hermes gateway run`,stdout/stderr 重定向到 log 文件。
|
|
523
|
+
* 子进程被 unref,pair 退出后 gateway 继续跑。
|
|
524
|
+
*
|
|
525
|
+
* @param {{ binary: string, logPath: string, env?: NodeJS.ProcessEnv, log?: any }} opts
|
|
526
|
+
* @returns {{ pid: number, logPath: string }}
|
|
527
|
+
*/
|
|
528
|
+
export function spawnGatewayDetached({ binary, logPath, env, log }) {
|
|
529
|
+
// 保证 log 目录存在
|
|
530
|
+
try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); } catch { /* ok */ }
|
|
531
|
+
// 'a' 追加模式 —— 多次启动日志连续
|
|
532
|
+
const fd = fs.openSync(logPath, "a");
|
|
533
|
+
// `--replace`: hermes 自家的"自动替换已存在 gateway"标志(见用户报的错误
|
|
534
|
+
// 提示:"use 'hermes gateway run --replace' to auto-replace")。我们前面
|
|
535
|
+
// 已经 stopGatewayOnPort 杀过一遍,--replace 是 TOCTOU 兜底 —— 万一在
|
|
536
|
+
// kill 和 spawn 之间又被人拉起来(launchd KeepAlive 之类),hermes 自己
|
|
537
|
+
// 会处理。老版本 hermes 没这个 flag 会报 unknown option 立刻退出,
|
|
538
|
+
// waitForHealth 30s 后 caller 报错可见。
|
|
539
|
+
const child = spawn(binary, ["gateway", "run", "--replace"], {
|
|
540
|
+
detached: true,
|
|
541
|
+
stdio: ["ignore", fd, fd],
|
|
542
|
+
env: { ...process.env, ...(env || {}) },
|
|
543
|
+
cwd: process.env.HOME || "/",
|
|
544
|
+
});
|
|
545
|
+
// 让 stdio FD 不阻塞 GC
|
|
546
|
+
try { fs.closeSync(fd); } catch { /* ok */ }
|
|
547
|
+
// 监听 error 事件防止 ENOENT 等错误变成 uncaughtException
|
|
548
|
+
child.on("error", (err) => {
|
|
549
|
+
log?.warn?.("hermes.gateway.spawn.error", "gateway child process error", {
|
|
550
|
+
binary, error: err?.message || String(err),
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
child.unref();
|
|
554
|
+
log?.info?.("hermes.gateway.spawn", "spawned detached", {
|
|
555
|
+
binary, pid: child.pid, log_path: logPath,
|
|
556
|
+
});
|
|
557
|
+
return { pid: child.pid, logPath };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 顶层入口 —— 把"配置改了,请重启 gateway"自动化。
|
|
562
|
+
*
|
|
563
|
+
* 输入 `mode`:
|
|
564
|
+
* - "ensure" gateway 已通过 /health 就不动;不通就 spawn
|
|
565
|
+
* - "restart" 无条件 stop + spawn(配置实际有变化时)
|
|
566
|
+
*
|
|
567
|
+
* 返回 { ok, started, restarted, alreadyOk, error, logPath, pid }
|
|
568
|
+
*/
|
|
569
|
+
export async function manageHermesGateway({
|
|
570
|
+
baseUrl, mode = "ensure", logPath, log, healthTimeoutMs,
|
|
571
|
+
} = {}) {
|
|
572
|
+
const port = Number(new URL(baseUrl).port || "8642");
|
|
573
|
+
const eLog = log?.child ? log.child({ comp: "hermes.gateway" }) : log;
|
|
574
|
+
const effectiveLog = logPath || path.join(os.tmpdir(), "hermes-gateway.log");
|
|
575
|
+
|
|
576
|
+
// mode=ensure:先 probe,已经通就返回
|
|
577
|
+
if (mode === "ensure") {
|
|
578
|
+
const pre = await probeHealth(baseUrl);
|
|
579
|
+
if (pre.ok) {
|
|
580
|
+
eLog?.info?.("hermes.gateway.ensure.already_ok", "gateway healthy", { baseUrl });
|
|
581
|
+
return { ok: true, alreadyOk: true, started: false, restarted: false };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const binary = findHermesBinary();
|
|
586
|
+
if (!binary) {
|
|
587
|
+
return {
|
|
588
|
+
ok: false, started: false, restarted: false,
|
|
589
|
+
error: "hermes 可执行文件没在 PATH 找到(也试了 ~/.local/bin / ~/.hermes/.../venv/bin / /usr/local/bin / /opt/homebrew/bin)。装 hermes-agent 后再 pair。",
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 无论 mode 一律先 stop —— mode=ensure 走到这说明 probe 已经失败,gateway
|
|
594
|
+
// 要么没起、要么进程在但没 listen :8642(典型 API_SERVER_ENABLED=false 状态)。
|
|
595
|
+
// 后者 stopGatewayOnPort 通过 pid file 也能找到并杀掉;前者就是 no-op。
|
|
596
|
+
// 不 stop 直接 spawn 会撞 hermes 自家 lock → "Gateway already running" → 死循环。
|
|
597
|
+
const stopResult = await stopGatewayOnPort(port, { log: eLog });
|
|
598
|
+
|
|
599
|
+
// spawn 新进程
|
|
600
|
+
const { pid, logPath: lp } = spawnGatewayDetached({
|
|
601
|
+
binary, logPath: effectiveLog, log: eLog,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// 轮询 /health。healthTimeoutMs 可由调用方收短(配对场景不该为等 gateway
|
|
605
|
+
// 健康而阻塞后续步骤 —— gateway 已 detached spawn,会在后台继续起来)。
|
|
606
|
+
const effectiveHealthTimeout = healthTimeoutMs || HEALTH_POLL_TIMEOUT_MS;
|
|
607
|
+
const final = await waitForHealth(baseUrl, { timeoutMs: effectiveHealthTimeout });
|
|
608
|
+
if (final.ok) {
|
|
609
|
+
return {
|
|
610
|
+
ok: true, started: true,
|
|
611
|
+
restarted: !!(stopResult && stopResult.stopped),
|
|
612
|
+
pid, logPath: lp,
|
|
613
|
+
stoppedOldPid: stopResult?.pid,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
// 启动了但探活超时 —— 多半是配置错(API key 缺 / venv 损坏),让用户看 log。
|
|
617
|
+
// started=true 表示进程已 spawn(detached),只是没在窗口内通过 /health;
|
|
618
|
+
// 调用方据此区分「后台启动中/未配置模型」(软) 与「连进程都没起」(硬)。
|
|
619
|
+
return {
|
|
620
|
+
ok: false, started: true, healthTimedOut: true,
|
|
621
|
+
restarted: !!(stopResult && stopResult.stopped),
|
|
622
|
+
pid, logPath: lp,
|
|
623
|
+
error: `gateway 已 spawn 但 ${effectiveHealthTimeout}ms 内未通过 /health。看启动日志:${lp}`,
|
|
624
|
+
};
|
|
625
|
+
}
|