@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,1346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slashCommandRouter.mjs — Bridge-side slash command interception.
|
|
3
|
+
*
|
|
4
|
+
* 远程 chat 通道里收到 `/foo` 时怎么处理:
|
|
5
|
+
*
|
|
6
|
+
* not-slash — 普通文本或 claude 上要透传给 CLI 的 slash, bridge 不拦
|
|
7
|
+
* passthrough — 标记 slash 但仍透传给 CLI(claude 部分命令 CLI 真支持)
|
|
8
|
+
* client-reset — bridge 自己处理 + 清 thread→session 映射(/clear)
|
|
9
|
+
* bridge-synth — bridge 合成响应, **不**调用 CLI
|
|
10
|
+
* unsupported — 极少见 fallback(理论上 default-export 之后不会出现)
|
|
11
|
+
*
|
|
12
|
+
* 设计原则:
|
|
13
|
+
* - claude `--print --input-format stream-json` 模式对部分 slash 会发
|
|
14
|
+
* synthetic assistant 消息(model="<synthetic>"), parser 端已经能透出来。
|
|
15
|
+
* 这些命令走 not-slash 让 CLI 自己说话。
|
|
16
|
+
* - codex MCP `tools/call codex` 不识别任何 slash, 一律 bridge-synth, 避免
|
|
17
|
+
* LLM 拿到 slash 当 prompt 幻觉应答(典型隐性事故 /clear)。
|
|
18
|
+
* - 凡是远程语境下没意义的命令(账号/退出),已经从 App 端 seed 移除, 不会
|
|
19
|
+
* 再传过来; 但 router 仍兜底返回简短说明, 万一用户手动打。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
import { execCliSafe, probeClaudeSystemInit } from "./cliExec.mjs";
|
|
27
|
+
import { collectCodexCustomCommands } from "../codex/codexCustomCommandsCollector.mjs";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 进程级缓存 claude system/init 输出。第一次实测 spawn 一次, 后续命中复用。
|
|
31
|
+
* Bridge 重启清空(claude CLI 升级后会自然 re-probe)。
|
|
32
|
+
*
|
|
33
|
+
* 缓存按 cwd 分键:init 里的 slash_commands 含**工作区级**自定义命令
|
|
34
|
+
* (.claude/commands/),不同工作区清单不同,混用会把 A 工作区的自定义命令
|
|
35
|
+
* 误判成「不支持」(P0#25)。forceFresh 给「未知命令」路径用——用户刚创建的
|
|
36
|
+
* 自定义命令不在旧缓存里,必须绕过缓存重探一次才能判死。
|
|
37
|
+
*/
|
|
38
|
+
let _claudeInitCache = null; // { cwd, init }
|
|
39
|
+
async function getClaudeInit(ctx, { forceFresh = false } = {}) {
|
|
40
|
+
// 用 "in" 检测 key 是否存在,允许注入明确的 null(probe 失败场景的测试)
|
|
41
|
+
if (ctx && "claudeInit" in ctx) return ctx.claudeInit;
|
|
42
|
+
const cwd = ctx?.cwd || "";
|
|
43
|
+
if (!forceFresh && _claudeInitCache && _claudeInitCache.cwd === cwd) {
|
|
44
|
+
return _claudeInitCache.init;
|
|
45
|
+
}
|
|
46
|
+
const init = await probeClaudeSystemInit({ cwd: ctx?.cwd, timeoutMs: 8000 });
|
|
47
|
+
if (init) _claudeInitCache = { cwd, init };
|
|
48
|
+
return init;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 允许 `:` 以匹配插件/自定义命令命名空间(如 /my-plugin:init)。
|
|
52
|
+
const SLASH_RE = /^\s*(\/[a-zA-Z][\w:-]*)(?:\s+([\s\S]*))?\s*$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 解析并分类。
|
|
56
|
+
*
|
|
57
|
+
* @param {string} input
|
|
58
|
+
* @param {"claude"|"codex"|string} runtime
|
|
59
|
+
* @param {object} [bridgeCtx] — bridge 状态, 用于合成 codex 响应:
|
|
60
|
+
* {
|
|
61
|
+
* version: string, // CLI 版本
|
|
62
|
+
* cwd: string, // bridge 工作目录
|
|
63
|
+
* gwId: string, // bridge gw_id
|
|
64
|
+
* threadId: string, // agentlink thread_id
|
|
65
|
+
* sessionId: string|null, // CLI internal session/threadId
|
|
66
|
+
* model: string|null, // 当前 model
|
|
67
|
+
* sessionConfigured: {...}|null, // codex MCP session_configured payload
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
export async function classifySlashCommand(input, runtime, bridgeCtx) {
|
|
71
|
+
const m = SLASH_RE.exec(typeof input === "string" ? input : "");
|
|
72
|
+
if (!m) return { kind: "not-slash" };
|
|
73
|
+
const command = m[1].toLowerCase();
|
|
74
|
+
const args = (m[2] || "").trim();
|
|
75
|
+
const ctx = bridgeCtx || {};
|
|
76
|
+
|
|
77
|
+
if (runtime === "claude") {
|
|
78
|
+
// /clear: 必须 bridge 层拦截清 session map
|
|
79
|
+
if (command === "/clear") {
|
|
80
|
+
return {
|
|
81
|
+
kind: "client-reset",
|
|
82
|
+
command,
|
|
83
|
+
ackText: "已清除 Claude Code 远程会话上下文,下一条消息将从新会话开始。",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// CLI 真正会执行的命令 → passthrough 让 CLI 自己跑(保留 args,原文透传)
|
|
87
|
+
if (CLAUDE_CLI_HANDLED.has(command)) {
|
|
88
|
+
return { kind: "passthrough", command };
|
|
89
|
+
}
|
|
90
|
+
// TUI 对齐: print 模式不可用但有明确动作语义的命令 → 改写为等效 prompt 真跑
|
|
91
|
+
if (CLAUDE_PROMPT_REWRITES[command]) {
|
|
92
|
+
return {
|
|
93
|
+
kind: "prompt-rewrite",
|
|
94
|
+
command,
|
|
95
|
+
rewrittenInput: CLAUDE_PROMPT_REWRITES[command](args),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// 插件/自定义命令命名空间(带 :)→ 一律透传,存在性交给 claude 校验
|
|
99
|
+
if (command.includes(":")) {
|
|
100
|
+
return { kind: "passthrough", command };
|
|
101
|
+
}
|
|
102
|
+
// bridge 有等价合成实现的命令 → bridge-synth
|
|
103
|
+
if (CLAUDE_HANDLERS[command]) {
|
|
104
|
+
const text = await synthesizeClaudeSlashResponse(command, args, ctx);
|
|
105
|
+
if (text != null) {
|
|
106
|
+
// handler 返回 { __kind: "interrupt_session_pick" } 信号对象时,改为 interrupt-session-pick kind
|
|
107
|
+
if (typeof text === "object" && text.__kind === "interrupt_session_pick") {
|
|
108
|
+
return { kind: "interrupt-session-pick", command };
|
|
109
|
+
}
|
|
110
|
+
return { kind: "bridge-synth", command, responseText: text };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "not-slash" };
|
|
113
|
+
}
|
|
114
|
+
// 未知命令:探 claude system/init 的 slash_commands 活体清单。
|
|
115
|
+
// 命中(用户自定义 ~/.claude/commands / skill 命令)→ 透传真跑;
|
|
116
|
+
// 未命中 → __default__ 合成提示,避免英文 "Unknown command" 直出。
|
|
117
|
+
const bare = command.slice(1);
|
|
118
|
+
const hitKnown = (init) => {
|
|
119
|
+
const known = Array.isArray(init?.slash_commands) ? init.slash_commands : [];
|
|
120
|
+
return known.some((c) => String(c).toLowerCase() === bare);
|
|
121
|
+
};
|
|
122
|
+
let init = await getClaudeInit(ctx);
|
|
123
|
+
let hit = hitKnown(init);
|
|
124
|
+
if (!hit && !ctx?.claudeInit) {
|
|
125
|
+
// 缓存可能 stale:用户刚创建的自定义命令不在旧清单里 (P0#25)。
|
|
126
|
+
// 判死前强制 re-probe 一次(未知命令低频,~1s 探活成本可接受)。
|
|
127
|
+
init = await getClaudeInit(ctx, { forceFresh: true });
|
|
128
|
+
hit = hitKnown(init);
|
|
129
|
+
}
|
|
130
|
+
if (hit) {
|
|
131
|
+
return { kind: "passthrough", command };
|
|
132
|
+
}
|
|
133
|
+
// 把 probe 结果注入 ctx,让 __default__ 区分「probe 成功 miss」和「probe null」
|
|
134
|
+
const ctxWithInit = { ...ctx, _claudeProbeInit: init };
|
|
135
|
+
const text = await synthesizeClaudeSlashResponse(command, args, ctxWithInit);
|
|
136
|
+
return { kind: "bridge-synth", command, responseText: text };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (runtime === "codex") {
|
|
140
|
+
if (command === "/clear" || command === "/new") {
|
|
141
|
+
return {
|
|
142
|
+
kind: "client-reset",
|
|
143
|
+
command,
|
|
144
|
+
ackText: command === "/clear"
|
|
145
|
+
? "已清除 Codex 远程会话上下文,下一条消息将从新会话开始。"
|
|
146
|
+
: "已开新 Codex 会话(上下文已清空),发消息开始对话。",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// bridge-synth 内置命令优先(防止同名自定义命令覆盖 /help /status 等)
|
|
150
|
+
if (CODEX_HANDLERS[command]) {
|
|
151
|
+
const text = await synthesizeCodexSlashResponse(command, args, ctx);
|
|
152
|
+
if (text != null) {
|
|
153
|
+
if (typeof text === "object" && text.__kind === "interrupt_session_pick") {
|
|
154
|
+
return { kind: "interrupt-session-pick", command };
|
|
155
|
+
}
|
|
156
|
+
return { kind: "bridge-synth", command, responseText: text };
|
|
157
|
+
}
|
|
158
|
+
return { kind: "not-slash" };
|
|
159
|
+
}
|
|
160
|
+
// codex 自定义命令 expand 路径
|
|
161
|
+
{
|
|
162
|
+
const bare = command.slice(1);
|
|
163
|
+
const promptsDir = ctx?._codexPromptsDir || join(homedir(), ".codex", "prompts");
|
|
164
|
+
// 用注入的 stale 列表(测试)或从磁盘扫描
|
|
165
|
+
let customCmds;
|
|
166
|
+
if (ctx?._codexCustomCommands) {
|
|
167
|
+
customCmds = ctx._codexCustomCommands;
|
|
168
|
+
} else {
|
|
169
|
+
customCmds = await collectCodexCustomCommands({ _promptsDir: promptsDir });
|
|
170
|
+
}
|
|
171
|
+
const isCustom = customCmds.some((c) => c.command === command);
|
|
172
|
+
if (isCustom) {
|
|
173
|
+
const promptFile = join(promptsDir, `${bare}.md`);
|
|
174
|
+
if (!existsSync(promptFile)) {
|
|
175
|
+
// stale 缓存场景:命令在列表但文件不存在 → 兜底 not-slash
|
|
176
|
+
return { kind: "not-slash" };
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const rawContent = readFileSync(promptFile, "utf8");
|
|
180
|
+
// 去掉 frontmatter,只取正文
|
|
181
|
+
const bodyMatch = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(rawContent);
|
|
182
|
+
const body = bodyMatch ? bodyMatch[1].trim() : rawContent.trim();
|
|
183
|
+
// 占位符替换
|
|
184
|
+
const tokens = args ? args.split(/\s+/) : [];
|
|
185
|
+
let expanded = body
|
|
186
|
+
.replace(/\$ARGUMENTS/g, args)
|
|
187
|
+
.replace(/\$([1-9])/g, (_, n) => tokens[Number(n) - 1] ?? "");
|
|
188
|
+
return { kind: "codex-custom-expand", command, expandedPrompt: expanded };
|
|
189
|
+
} catch {
|
|
190
|
+
return { kind: "not-slash" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 未知命令:走 codex __default__ 合成
|
|
195
|
+
const text = await synthesizeCodexSlashResponse(command, args, ctx);
|
|
196
|
+
if (text != null) {
|
|
197
|
+
if (typeof text === "object" && text.__kind === "interrupt_session_pick") {
|
|
198
|
+
return { kind: "interrupt-session-pick", command };
|
|
199
|
+
}
|
|
200
|
+
return { kind: "bridge-synth", command, responseText: text };
|
|
201
|
+
}
|
|
202
|
+
return { kind: "not-slash" };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (runtime === "hermes") {
|
|
206
|
+
if (command === "/clear" || command === "/new") {
|
|
207
|
+
return {
|
|
208
|
+
kind: "client-reset",
|
|
209
|
+
command,
|
|
210
|
+
ackText: command === "/clear"
|
|
211
|
+
? "已清除当前 Hermes 会话历史,下一条消息将从新会话开始。"
|
|
212
|
+
: "已开新 Hermes 会话(历史清空),发消息开始对话。",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const text = await synthesizeHermesSlashResponse(command, args, ctx);
|
|
216
|
+
if (text != null) {
|
|
217
|
+
if (typeof text === "object" && text.__kind === "interrupt_session_pick") {
|
|
218
|
+
return { kind: "interrupt-session-pick", command };
|
|
219
|
+
}
|
|
220
|
+
return { kind: "bridge-synth", command, responseText: text };
|
|
221
|
+
}
|
|
222
|
+
return { kind: "not-slash" };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { kind: "not-slash" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Claude CLI 在 `--print --input-format stream-json` 下会真实执行的命令,
|
|
229
|
+
* bridge 原文透传即可(synthetic 快回复由 stdoutParser 抓 text, LLM-backed
|
|
230
|
+
* 长任务走正常事件流, 远程照样可流式看到进度)。
|
|
231
|
+
*
|
|
232
|
+
* 注: /init /review /security-review 是 LLM-backed 长任务(分钟级)。曾经为它们
|
|
233
|
+
* bridge-synth 说明文档, 但用户预期是「真的执行」(P0: /init + 跟随内容被丢弃,
|
|
234
|
+
* 只回帮助文案), 故改为透传真跑, 跟随内容(args)随原文一起进入 claude。 */
|
|
235
|
+
const CLAUDE_CLI_HANDLED = new Set([
|
|
236
|
+
"/compact", // 真跑 compaction(秒级返回, 即使无内容也快)
|
|
237
|
+
"/context", // 显示 token 用量表(同步, 秒级)
|
|
238
|
+
"/init", // 真跑 CLAUDE.md 初始化(LLM-backed, 流式可见)
|
|
239
|
+
"/review", // 真跑代码评审
|
|
240
|
+
"/security-review", // 真跑安全审查
|
|
241
|
+
"/cost", // 实测(claude 2.1.170): print 模式真返回订阅配额, 弃用 PTY 探测
|
|
242
|
+
"/usage", // 同上
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
/** TUI 对齐(P1#22): 这些命令 print 模式不可用("isn't available"), 但 TUI 里有
|
|
246
|
+
* 明确动作语义 —— 改写成等效自然语言 prompt 让 claude 真跑, 行为与 TUI 一致。
|
|
247
|
+
* (实测 2.1.170: /diff /recap 等在 print 模式只回 "isn't available"。) */
|
|
248
|
+
const CLAUDE_PROMPT_REWRITES = {
|
|
249
|
+
"/diff": (args) =>
|
|
250
|
+
args
|
|
251
|
+
? `请用 Bash 工具执行 git diff ${args},把结果按文件整理返回(带代码块格式)。`
|
|
252
|
+
: "请用 Bash 工具执行 git status 和 git diff(如有 staged 改动再补 git diff --cached),把当前改动按文件整理返回(带代码块格式)。",
|
|
253
|
+
"/recap": (args) =>
|
|
254
|
+
args
|
|
255
|
+
? `请回顾我们这个会话的内容,围绕「${args}」做一个结构化总结:做了什么、改了哪些文件、还有什么待办。`
|
|
256
|
+
: "请回顾我们这个会话的内容,做一个结构化总结:聊了什么、做了哪些改动、结论与待办事项。",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
260
|
+
// Claude synth — 所有 seed 命令保证有响应
|
|
261
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
async function synthesizeClaudeSlashResponse(command, args, ctx) {
|
|
264
|
+
const handler = CLAUDE_HANDLERS[command];
|
|
265
|
+
if (handler) return await handler(ctx, args, command);
|
|
266
|
+
// 未知 slash → 走 __default__, 保证用户永远不会拿到"暂不支持"那种空洞回复
|
|
267
|
+
return await CLAUDE_HANDLERS.__default__(ctx, args, command);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const CLAUDE_HANDLERS = {
|
|
271
|
+
"/help": (ctx) => [
|
|
272
|
+
"Claude Code 远程会话命令清单(完整版):",
|
|
273
|
+
"",
|
|
274
|
+
"── 会话控制 ─────────────────────────────",
|
|
275
|
+
" /clear 清除上下文, 下一条消息开新会话",
|
|
276
|
+
" /compact 压缩历史以释放上下文窗口(CLI 真跑)",
|
|
277
|
+
" /context 查看当前 token 用量分布(CLI 真显示)",
|
|
278
|
+
" /resume 续接最近会话(本机才可用,远程由 thread 续接代替)",
|
|
279
|
+
" /rewind 回退到上一轮(远程不可用)",
|
|
280
|
+
" /branch 分支当前会话(本机才可用)",
|
|
281
|
+
" /recap 快速回顾当前会话",
|
|
282
|
+
" /rename 重命名当前会话",
|
|
283
|
+
"",
|
|
284
|
+
"── 信息查看 ─────────────────────────────",
|
|
285
|
+
" /status 会话状态(版本/cwd/gw_id/thread_id)",
|
|
286
|
+
" /model 当前模型",
|
|
287
|
+
" /cost /usage 订阅/计费信息",
|
|
288
|
+
" /diff git diff(本机才可用, 远程直接让 AI 跑)",
|
|
289
|
+
" /doctor 健康检查",
|
|
290
|
+
" /export 导出会话历史",
|
|
291
|
+
"",
|
|
292
|
+
"── 配置 / 管理 ────────────────────────",
|
|
293
|
+
" /config 配置概览",
|
|
294
|
+
" /permissions 权限设置说明",
|
|
295
|
+
" /hooks hooks 配置",
|
|
296
|
+
" /mcp MCP server 列表",
|
|
297
|
+
" /memory 记忆系统说明",
|
|
298
|
+
"",
|
|
299
|
+
"── 智能体 / 技能 ──────────────────────",
|
|
300
|
+
" /agents subagent 列表",
|
|
301
|
+
" /skills skills 库说明",
|
|
302
|
+
"",
|
|
303
|
+
"── 评审 ─────────────────────────────────",
|
|
304
|
+
" /review 代码评审(CLI 真跑)",
|
|
305
|
+
" /security-review 安全审查(CLI 真跑)",
|
|
306
|
+
"",
|
|
307
|
+
"── 反馈 ─────────────────────────────────",
|
|
308
|
+
" /feedback 提交反馈",
|
|
309
|
+
"",
|
|
310
|
+
"用法: 在输入框点击「系统指令」chip 选命令,或直接打 `/xxx` 发送。",
|
|
311
|
+
].join("\n"),
|
|
312
|
+
|
|
313
|
+
"/status": async (ctx) => {
|
|
314
|
+
const init = await getClaudeInit(ctx);
|
|
315
|
+
const lines = ["Claude Code 远程会话状态:"];
|
|
316
|
+
lines.push(` Claude Code 版本 : ${init?.claude_code_version || ctx.version || "未知"}`);
|
|
317
|
+
lines.push(` 当前模型 : ${init?.model || ctx.model || "未知"}`);
|
|
318
|
+
lines.push(` 工作目录(cwd) : ${init?.cwd || ctx.cwd || "未知"}`);
|
|
319
|
+
if (init?.permissionMode) lines.push(` 权限模式 : ${init.permissionMode}`);
|
|
320
|
+
if (init?.output_style) lines.push(` 输出风格 : ${init.output_style}`);
|
|
321
|
+
if (init?.apiKeySource) lines.push(` API key 来源 : ${init.apiKeySource}`);
|
|
322
|
+
if (Array.isArray(init?.mcp_servers)) lines.push(` MCP server 数 : ${init.mcp_servers.length}`);
|
|
323
|
+
if (Array.isArray(init?.agents)) lines.push(` Subagent 数 : ${init.agents.length}`);
|
|
324
|
+
if (Array.isArray(init?.skills)) lines.push(` Skill 数 : ${init.skills.length}`);
|
|
325
|
+
if (ctx.gwId) lines.push(` Bridge gateway id : ${ctx.gwId}`);
|
|
326
|
+
if (ctx.threadId) lines.push(` 对话 thread id : ${ctx.threadId}`);
|
|
327
|
+
lines.push(` claude session id : ${ctx.sessionId || "(尚未建立, 下一条消息时分配)"}`);
|
|
328
|
+
lines.push("");
|
|
329
|
+
lines.push("精确 token 用量请发 /context 查看, 速率限制发 /usage。");
|
|
330
|
+
return lines.join("\n");
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
"/model": async (ctx) => {
|
|
334
|
+
const init = await getClaudeInit(ctx);
|
|
335
|
+
if (init?.model) {
|
|
336
|
+
const lines = [`当前 Claude Code 模型(实时, 从 claude --print 探到):`];
|
|
337
|
+
lines.push(` ▸ 模型 id : ${init.model}`);
|
|
338
|
+
if (init.apiKeySource) lines.push(` ▸ API key 来源: ${init.apiKeySource}`);
|
|
339
|
+
if (init.claude_code_version) lines.push(` ▸ CLI 版本 : ${init.claude_code_version}`);
|
|
340
|
+
lines.push("");
|
|
341
|
+
lines.push("切换模型: 在 App 顶部「模型」入口选择, 或在本机 ~/.claude/settings.json");
|
|
342
|
+
lines.push("`model` 字段改后重启 bridge。");
|
|
343
|
+
return lines.join("\n");
|
|
344
|
+
}
|
|
345
|
+
if (ctx.model) return `当前 Claude Code 模型: ${ctx.model}\n\n切换模型请用 App 顶部「模型」入口。`;
|
|
346
|
+
return "当前 Claude Code 模型未知(probeClaudeSystemInit 超时)。请用 App 顶部「模型」入口选择, 或在本机终端 `claude` 后 /model 查看。";
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
"/version": async (ctx) => {
|
|
350
|
+
const real = await execCliSafe("claude", ["--version"], { timeoutMs: 4000 });
|
|
351
|
+
if (real) return `Claude Code 版本(实时):\n ${real}`;
|
|
352
|
+
return ctx.version ? `Claude Code ${ctx.version}` : "Claude Code 版本未知";
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
// /cost /usage 已移入 CLAUDE_CLI_HANDLED 透传真跑(P1#22),不再 PTY 探测。
|
|
356
|
+
|
|
357
|
+
"/agents": () => [
|
|
358
|
+
"Claude Code subagent 列表(管理 UI 限本机交互模式):",
|
|
359
|
+
"",
|
|
360
|
+
" ▸ 远程会话仍能用 subagent:",
|
|
361
|
+
" - 普通对话里直接说「让 code-reviewer 审一下当前 diff」",
|
|
362
|
+
" 或「派一个 general-purpose agent 搜全仓里的 TODO」",
|
|
363
|
+
" - Claude 会自动 dispatch Task / Agent 工具调用相应 subagent",
|
|
364
|
+
" - subagent 输出在 App 里以「subagent_ref」事件块展示(可展开看子任务详情)",
|
|
365
|
+
"",
|
|
366
|
+
" ▸ 你装了哪些自定义 subagent,可在本机终端 `claude` 进 REPL 后",
|
|
367
|
+
" 输入 `/agents` 查看完整管理 UI。",
|
|
368
|
+
].join("\n"),
|
|
369
|
+
|
|
370
|
+
"/skills": () => [
|
|
371
|
+
"Claude Code Skills:",
|
|
372
|
+
"",
|
|
373
|
+
" ▸ Skill 是 Claude Code 的「触发名 + 指令包」机制, 比如 `/init` 实际就是",
|
|
374
|
+
" 一个 skill。用法: 在对话里直接打 `/<skill-name>` 即可触发。",
|
|
375
|
+
" ▸ 完整 skill 管理(查看 / 启用 / 禁用 / 安装新 skill) 仅在本机交互模式可用。",
|
|
376
|
+
" 本机终端执行 `claude`, 进 REPL 后输入 `/skills` 看完整 UI。",
|
|
377
|
+
" ▸ 第三方 plugins 也会自动注册 skill,远程能用,但管理走本机。",
|
|
378
|
+
].join("\n"),
|
|
379
|
+
|
|
380
|
+
"/memory": () => [
|
|
381
|
+
"Claude Code 记忆(auto-memory)系统:",
|
|
382
|
+
"",
|
|
383
|
+
" ▸ Claude 会自动把对话里值得记住的信息(用户偏好/项目惯例/反馈/重要决定)",
|
|
384
|
+
" 写入 ~/.claude/projects/<project>/memory/ 下的 MEMORY.md 索引 + 独立",
|
|
385
|
+
" 分类 markdown 文件。每次对话开始 Claude 会自动加载 MEMORY.md 作为",
|
|
386
|
+
" 系统提示一部分。",
|
|
387
|
+
" ▸ 远程会话也会触发自动记忆。手动让 Claude 记: 直接在对话里说",
|
|
388
|
+
" 「记住:我们这边项目用 ...」就行。",
|
|
389
|
+
" ▸ 完整记忆管理 UI(查看/编辑/删除单条记忆)仅本机 REPL 模式 `/memory` 可用。",
|
|
390
|
+
].join("\n"),
|
|
391
|
+
|
|
392
|
+
"/permissions": async (ctx) => {
|
|
393
|
+
const init = await getClaudeInit(ctx);
|
|
394
|
+
const lines = ["Claude Code 权限配置(实时, 从 claude --print system/init 探到):"];
|
|
395
|
+
if (init?.permissionMode) lines.push(` ▸ 当前 permission mode : ${init.permissionMode}`);
|
|
396
|
+
else lines.push(" ▸ 当前 permission mode : 未知(探测超时)");
|
|
397
|
+
if (Array.isArray(init?.tools)) lines.push(` ▸ 当前可用工具数 : ${init.tools.length}`);
|
|
398
|
+
if (init?.apiKeySource) lines.push(` ▸ API key 来源 : ${init.apiKeySource}`);
|
|
399
|
+
lines.push("");
|
|
400
|
+
lines.push("permissionMode 取值:");
|
|
401
|
+
lines.push(" • default — 工具调用前弹审批");
|
|
402
|
+
lines.push(" • acceptEdits — Edit/Write 自动通过, 其它仍审批");
|
|
403
|
+
lines.push(" • bypassPermissions — 全部自动通过(--print 远程会话默认)");
|
|
404
|
+
lines.push(" • plan — 只跑 plan, 不执行写操作");
|
|
405
|
+
lines.push(" • auto — 按工具类别自动判");
|
|
406
|
+
lines.push("");
|
|
407
|
+
lines.push("修改: 编辑 ~/.claude/settings.json 的 `permissions` / `permissionMode`,");
|
|
408
|
+
lines.push("或编辑项目 `.claude/settings.json`, 重启 bridge 生效。");
|
|
409
|
+
lines.push("");
|
|
410
|
+
lines.push("── AgentLink Bridge 审批模式 ────────────────────────────────");
|
|
411
|
+
lines.push("AGENTLINK_CLAUDE_PERMISSION_MODE=approval 启用 App 审批弹窗");
|
|
412
|
+
lines.push(" • 每次需要权限的工具调用, App 弹出「同意/拒绝」审批卡。");
|
|
413
|
+
lines.push(" • 10 分钟无响应自动 deny(不会永久阻塞)。");
|
|
414
|
+
lines.push(" ⚠️ 重要:approval 模式使用 --setting-sources project,");
|
|
415
|
+
lines.push(" ~/.claude/settings.json 中配置的工具 allow 规则(如 Bash(*))");
|
|
416
|
+
lines.push(" 在此模式下不生效。如需自动放行某些工具,请在工作区的");
|
|
417
|
+
lines.push(" .claude/settings.json 中配置。");
|
|
418
|
+
lines.push(" 默认值 bypassPermissions 不变,approval 为显式 opt-in。");
|
|
419
|
+
return lines.join("\n");
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
"/resume": () => ({ __kind: "interrupt_session_pick" }),
|
|
423
|
+
|
|
424
|
+
"/rewind": () => [
|
|
425
|
+
"Claude Code /rewind 不可用于远程会话:",
|
|
426
|
+
"",
|
|
427
|
+
" ▸ 远程模式 `claude --print` 单 turn 推进, 没有 REPL 的可视化时间线。",
|
|
428
|
+
" ▸ 想撤销上一轮: 删掉最近一条对话消息(App 里长按消息删除)即可。",
|
|
429
|
+
" ▸ 想从某个早期节点 fork: App 聊天列表新建对话即可。",
|
|
430
|
+
].join("\n"),
|
|
431
|
+
|
|
432
|
+
"/branch": () => [
|
|
433
|
+
"Claude Code /branch 不可用于远程会话:",
|
|
434
|
+
"",
|
|
435
|
+
" ▸ /branch 在本机 REPL 模式从当前点开新分支 session。",
|
|
436
|
+
" ▸ 远程语境下等价做法: App 聊天列表「新建对话」+ 把当前对话的关键上下文",
|
|
437
|
+
" 复制过去, 等于一个手动 branch。",
|
|
438
|
+
].join("\n"),
|
|
439
|
+
|
|
440
|
+
// /diff 已走 prompt-rewrite 真跑(P1#22)。
|
|
441
|
+
|
|
442
|
+
"/config": async (ctx) => {
|
|
443
|
+
const init = await getClaudeInit(ctx);
|
|
444
|
+
const lines = ["Claude Code 配置(实时, 从 claude --print system/init 探到):"];
|
|
445
|
+
lines.push(` ▸ Claude Code 版本 : ${init?.claude_code_version || ctx.version || "未知"}`);
|
|
446
|
+
lines.push(` ▸ 当前模型 : ${init?.model || ctx.model || "未知"}`);
|
|
447
|
+
lines.push(` ▸ 工作目录(cwd) : ${init?.cwd || ctx.cwd || "未知"}`);
|
|
448
|
+
if (init?.permissionMode) lines.push(` ▸ 权限模式 : ${init.permissionMode}`);
|
|
449
|
+
if (init?.output_style) lines.push(` ▸ 输出风格 : ${init.output_style}`);
|
|
450
|
+
if (init?.apiKeySource) lines.push(` ▸ API key 来源 : ${init.apiKeySource}`);
|
|
451
|
+
if (init?.analytics_disabled !== undefined) lines.push(` ▸ analytics : ${init.analytics_disabled ? "disabled" : "enabled"}`);
|
|
452
|
+
if (init?.fast_mode_state) lines.push(` ▸ fast mode : ${init.fast_mode_state}`);
|
|
453
|
+
if (Array.isArray(init?.mcp_servers)) lines.push(` ▸ MCP server 数 : ${init.mcp_servers.length}`);
|
|
454
|
+
if (Array.isArray(init?.agents)) lines.push(` ▸ Subagent 数 : ${init.agents.length}`);
|
|
455
|
+
if (Array.isArray(init?.skills)) lines.push(` ▸ Skill 数 : ${init.skills.length}`);
|
|
456
|
+
if (Array.isArray(init?.plugins)) lines.push(` ▸ Plugin 数 : ${init.plugins.length}`);
|
|
457
|
+
if (init?.memory_paths) lines.push(` ▸ 记忆路径 : ${JSON.stringify(init.memory_paths)}`);
|
|
458
|
+
lines.push("");
|
|
459
|
+
lines.push("完整配置看本机:");
|
|
460
|
+
lines.push(" ~/.claude/settings.json(用户级) / .claude/settings.json(项目级) /");
|
|
461
|
+
lines.push(" .claude/settings.local.json(本地级)");
|
|
462
|
+
lines.push("改后重启 bridge 生效。");
|
|
463
|
+
return lines.join("\n");
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
"/hooks": () => [
|
|
467
|
+
"Claude Code Hooks 配置:",
|
|
468
|
+
"",
|
|
469
|
+
" ▸ Hooks 在 ~/.claude/settings.json(或项目 .claude/settings.json)的 `hooks`",
|
|
470
|
+
" 字段配置,支持事件: PreToolUse / PostToolUse / SessionStart / SessionEnd /",
|
|
471
|
+
" UserPromptSubmit / Stop / Notification 等。",
|
|
472
|
+
" ▸ Hook 执行 shell 命令 / 脚本, 可以拦截 / 修改 / 拒绝工具调用。",
|
|
473
|
+
" ▸ 远程会话里 hooks 跟本机一样生效(bridge 起的 claude 进程读同一份配置)。",
|
|
474
|
+
" ▸ 想现场编辑 hooks 配置,请用本机终端: `claude config edit` 或直接改 settings.json。",
|
|
475
|
+
].join("\n"),
|
|
476
|
+
|
|
477
|
+
"/mcp": async () => {
|
|
478
|
+
// 真起 `claude mcp list` 拿用户级 + 项目级 MCP 配置(权威)
|
|
479
|
+
const real = await execCliSafe("claude", ["mcp", "list"], { timeoutMs: 5000 });
|
|
480
|
+
if (real && real.length > 0) {
|
|
481
|
+
return [
|
|
482
|
+
"Claude Code MCP server(实时, 来自 `claude mcp list`):",
|
|
483
|
+
"",
|
|
484
|
+
real,
|
|
485
|
+
"",
|
|
486
|
+
"管理: `claude mcp add <name> <cmd>` / `claude mcp remove <name>`,",
|
|
487
|
+
"或在 ~/.claude/settings.json 的 `mcpServers` 字段编辑。",
|
|
488
|
+
].join("\n");
|
|
489
|
+
}
|
|
490
|
+
return [
|
|
491
|
+
"Claude Code 当前没有挂载任何 MCP server(claude mcp list 输出空)。",
|
|
492
|
+
"",
|
|
493
|
+
"挂载方式:",
|
|
494
|
+
" ▸ 本机终端: `claude mcp add <name> <command>`(项目级)",
|
|
495
|
+
" ▸ 编辑 ~/.claude/settings.json 加 `mcpServers.<name>`(用户级)",
|
|
496
|
+
" ▸ 项目根加 .mcp.json(repo 级, 团队共享)",
|
|
497
|
+
].join("\n");
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
"/doctor": async (ctx) => {
|
|
501
|
+
const init = await getClaudeInit(ctx);
|
|
502
|
+
const lines = ["Claude Code 健康检查(远程探到):"];
|
|
503
|
+
lines.push(` ▸ Claude Code 版本 : ${init?.claude_code_version || ctx.version || "未知"}`);
|
|
504
|
+
lines.push(` ▸ Bridge 工作目录 : ${init?.cwd || ctx.cwd || "未知"}`);
|
|
505
|
+
lines.push(` ▸ Bridge gateway : ${ctx.gwId || "未知"}`);
|
|
506
|
+
lines.push(` ▸ probe 链路 : ${init ? "✓ claude --print system/init 探测成功" : "✗ 探测超时, 检查 claude binary 是否安装"}`);
|
|
507
|
+
if (init) {
|
|
508
|
+
lines.push(` ▸ 模型 / Mode : ${init.model || "?"} / ${init.permissionMode || "?"}`);
|
|
509
|
+
lines.push(` ▸ MCP / Agents : ${init.mcp_servers?.length ?? 0} server / ${init.agents?.length ?? 0} agent`);
|
|
510
|
+
lines.push(` ▸ Skills / Plugins : ${init.skills?.length ?? 0} skill / ${init.plugins?.length ?? 0} plugin`);
|
|
511
|
+
if (init.fast_mode_state) lines.push(` ▸ fast_mode_state : ${init.fast_mode_state}`);
|
|
512
|
+
}
|
|
513
|
+
lines.push("");
|
|
514
|
+
lines.push("更深的更新检查 / 配置语法校验请在本机终端跑 `claude doctor`(它会写");
|
|
515
|
+
lines.push("~/.claude/ 状态文件, 远程会话不触发避免污染本机配置)。");
|
|
516
|
+
return lines.join("\n");
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
"/export": () => [
|
|
520
|
+
"Claude Code 会话导出:",
|
|
521
|
+
"",
|
|
522
|
+
" ▸ 远程会话天然存档 — 整段事件流在服务端 thread 里, App 聊天页可滚动",
|
|
523
|
+
" 查看历史, 长按消息可分享 / 复制。",
|
|
524
|
+
" ▸ 本机磁盘上的 session jsonl 文件在 ~/.claude/projects/<cwd-hash>/<session>.jsonl,",
|
|
525
|
+
" 那是 claude --resume 用的内部状态, 不建议直接读。",
|
|
526
|
+
" ▸ 想批量导出多个 thread 的对话, 后续可以在 App「我的」页加导出入口(目前没有)。",
|
|
527
|
+
].join("\n"),
|
|
528
|
+
|
|
529
|
+
// /recap 已走 prompt-rewrite 真跑(P1#22)。
|
|
530
|
+
|
|
531
|
+
"/rename": () => [
|
|
532
|
+
"Claude Code 会话重命名:",
|
|
533
|
+
"",
|
|
534
|
+
" ▸ 远程会话的标题在 App 聊天列表里, 长按对应对话条目即可重命名。",
|
|
535
|
+
" ▸ 本机 REPL 的 `/rename` 改的是磁盘 session 文件名, 跟远程是两套独立存储。",
|
|
536
|
+
].join("\n"),
|
|
537
|
+
|
|
538
|
+
"/feedback": () => [
|
|
539
|
+
"提交 Claude Code / 本远程会话反馈:",
|
|
540
|
+
"",
|
|
541
|
+
" ▸ 直接在 App「我的」→「意见反馈」入口提交, 会带上 client 版本 + 当前",
|
|
542
|
+
" thread/run id, 帮我们快速定位。",
|
|
543
|
+
" ▸ Claude Code 本体 bug 也可以去 https://github.com/anthropics/claude-code/issues 报。",
|
|
544
|
+
].join("\n"),
|
|
545
|
+
|
|
546
|
+
// /init /review /security-review 已移入 CLAUDE_CLI_HANDLED 透传真跑(P0 修复),
|
|
547
|
+
// 不再 bridge-synth。
|
|
548
|
+
|
|
549
|
+
// ── 兜底: 任何未列入的 slash 都给一段简短提示, 同时建议改用 App / 自然语言
|
|
550
|
+
__default__: (ctx, _args, command) => {
|
|
551
|
+
// probe 成功(_claudeProbeInit 非 null)且命令 miss → 明确告知「未知指令」
|
|
552
|
+
if (ctx?._claudeProbeInit !== null && ctx?._claudeProbeInit !== undefined) {
|
|
553
|
+
return `未知指令——当前主机的 Claude Code 中未找到 ${command}。`;
|
|
554
|
+
}
|
|
555
|
+
// probe 失败/超时(init = null),无法断言命令不存在 → 保留原有含糊兜底
|
|
556
|
+
return [
|
|
557
|
+
`「${command}」是 Claude Code CLI 的本机交互命令,`,
|
|
558
|
+
"在远程会话语境下没有等价 UI。",
|
|
559
|
+
"",
|
|
560
|
+
"替代做法:",
|
|
561
|
+
" ▸ 直接用自然语言告诉 Claude 你想做什么,大部分 CLI 命令的功能都能",
|
|
562
|
+
" 通过对话完成(例如想看 git status,直接说「跑一下 git status」)。",
|
|
563
|
+
" ▸ 系统级操作请去本机终端 `claude` 跑。",
|
|
564
|
+
].join("\n");
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
569
|
+
// Codex synth — 所有 seed 命令保证有响应
|
|
570
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
async function synthesizeCodexSlashResponse(command, args, ctx) {
|
|
573
|
+
const handler = CODEX_HANDLERS[command];
|
|
574
|
+
if (handler) return await handler(ctx, args, command);
|
|
575
|
+
return await CODEX_HANDLERS.__default__(ctx, args, command);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const CODEX_HANDLERS = {
|
|
579
|
+
"/help": () => [
|
|
580
|
+
"Codex 远程会话命令清单(完整版, 全部走 bridge 合成):",
|
|
581
|
+
"",
|
|
582
|
+
"── 会话控制 ─────────────────────────────",
|
|
583
|
+
" /clear /new 开新会话(真 evict MCP 进程, fresh codex session)",
|
|
584
|
+
" /compact 上下文压缩(远程不支持, 用 /clear 替代)",
|
|
585
|
+
" /resume /fork 续接/分叉说明",
|
|
586
|
+
"",
|
|
587
|
+
"── 信息查看 ─────────────────────────────",
|
|
588
|
+
" /status 会话状态(model / cwd / approval / session id 等)",
|
|
589
|
+
" /model 当前模型 + provider",
|
|
590
|
+
" /cost 计费/用量说明",
|
|
591
|
+
"",
|
|
592
|
+
"── 权限 / 沙箱 / 审批 ────────────────────",
|
|
593
|
+
" /permissions 权限 profile(file_system + network 真值)",
|
|
594
|
+
" /sandbox 同 /permissions",
|
|
595
|
+
" /approvals 审批策略(approval_policy + reviewer 真值)",
|
|
596
|
+
"",
|
|
597
|
+
"── 工具 / 扩展 ──────────────────────────",
|
|
598
|
+
" /mcp 挂载的 MCP server 列表(真实)",
|
|
599
|
+
" /agent agent 配置说明",
|
|
600
|
+
" /apps apps 子系统说明",
|
|
601
|
+
" /plugins plugins 子系统说明",
|
|
602
|
+
" /personality personality 子系统说明",
|
|
603
|
+
" /experimental experimental flag 说明",
|
|
604
|
+
"",
|
|
605
|
+
"── 任务 / 控制 ──────────────────────────",
|
|
606
|
+
" /plan plan 模式说明",
|
|
607
|
+
" /fast fast 模式说明",
|
|
608
|
+
" /ps 进程/任务列表说明",
|
|
609
|
+
" /stop 停止当前任务说明",
|
|
610
|
+
" /mention @ 提及说明",
|
|
611
|
+
" /init 生成 AGENTS.md 提示",
|
|
612
|
+
" /diff git diff 说明",
|
|
613
|
+
" /review 代码评审说明",
|
|
614
|
+
" /copy 复制说明",
|
|
615
|
+
" /feedback 反馈入口",
|
|
616
|
+
"",
|
|
617
|
+
" /help 显示本帮助",
|
|
618
|
+
"",
|
|
619
|
+
"用法: 输入框点「系统指令」chip 选, 或直接打 `/xxx`。",
|
|
620
|
+
].join("\n"),
|
|
621
|
+
|
|
622
|
+
"/status": (ctx) => {
|
|
623
|
+
const cfg = ctx.sessionConfigured;
|
|
624
|
+
const lines = ["Codex 远程会话状态:"];
|
|
625
|
+
if (ctx.version) lines.push(` codex CLI 版本 : ${ctx.version}`);
|
|
626
|
+
if (cfg?.model) lines.push(` 模型 : ${cfg.model}${cfg.model_provider_id ? ` (provider=${cfg.model_provider_id})` : ""}`);
|
|
627
|
+
else if (ctx.model) lines.push(` 模型 : ${ctx.model}`);
|
|
628
|
+
if (cfg?.cwd || ctx.cwd) lines.push(` 工作目录(cwd) : ${cfg?.cwd || ctx.cwd}`);
|
|
629
|
+
if (cfg?.approval_policy) lines.push(` 审批策略 : ${cfg.approval_policy}`);
|
|
630
|
+
if (cfg?.reasoning_effort) lines.push(` 推理强度 : ${cfg.reasoning_effort}`);
|
|
631
|
+
if (ctx.gwId) lines.push(` Bridge gateway id : ${ctx.gwId}`);
|
|
632
|
+
if (ctx.threadId) lines.push(` 对话 thread id : ${ctx.threadId}`);
|
|
633
|
+
lines.push(` codex session id : ${cfg?.session_id || ctx.sessionId || "(尚未建立, 下一条消息时分配)"}`);
|
|
634
|
+
if (cfg?.captured_at) lines.push(` 数据采集时间 : ${cfg.captured_at}`);
|
|
635
|
+
if (!cfg) {
|
|
636
|
+
lines.push("");
|
|
637
|
+
lines.push("(尚无 codex MCP session_configured 数据 — 先发一条普通消息让");
|
|
638
|
+
lines.push(" bridge 启动 codex MCP, 再发 /status 就能看到 model / approval 真实配置。)");
|
|
639
|
+
}
|
|
640
|
+
return lines.join("\n");
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
"/cost": (ctx, args, cmd) => readCodexAuthCost(cmd || "/cost"),
|
|
644
|
+
// /usage: 跟 claude /usage 对齐, 显示 5h(primary) + weekly(secondary) 额度
|
|
645
|
+
"/usage": (ctx, args, cmd) => formatCodexUsage(ctx, cmd || "/usage"),
|
|
646
|
+
|
|
647
|
+
"/model": (ctx) => {
|
|
648
|
+
const cfg = ctx.sessionConfigured;
|
|
649
|
+
if (cfg?.model) {
|
|
650
|
+
const provider = cfg.model_provider_id ? ` (provider=${cfg.model_provider_id})` : "";
|
|
651
|
+
return `当前 Codex 模型: ${cfg.model}${provider}\n\n切换模型: 改 ~/.codex/config.toml 的 model 字段并重启 bridge, 或用 App 顶部「模型」入口。`;
|
|
652
|
+
}
|
|
653
|
+
if (ctx.model) return `当前 Codex 模型: ${ctx.model}\n\n切换模型: 改 ~/.codex/config.toml 后重启 bridge。`;
|
|
654
|
+
return "当前 Codex 模型未知(尚未与 codex MCP 建立 session)。先发一条普通消息触发 codex 启动, 然后再发 /model。";
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
"/version": async (ctx) => {
|
|
658
|
+
// 真起 `codex --version` 拿权威版本
|
|
659
|
+
const real = await execCliSafe("codex", ["--version"], { timeoutMs: 4000 });
|
|
660
|
+
if (real) return `Codex CLI 版本(实时):\n ${real}`;
|
|
661
|
+
return ctx.version ? `Codex CLI ${ctx.version}` : "Codex CLI 版本未知";
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
"/permissions": (ctx) => formatCodexPermissions(ctx.sessionConfigured),
|
|
665
|
+
"/sandbox": (ctx) => formatCodexPermissions(ctx.sessionConfigured),
|
|
666
|
+
|
|
667
|
+
"/approvals": (ctx) => {
|
|
668
|
+
const cfg = ctx.sessionConfigured;
|
|
669
|
+
if (!cfg) return "审批策略未知 — 尚未与 codex MCP 建立 session。先发一条普通消息触发 codex 启动,再发 /approvals。";
|
|
670
|
+
const lines = ["Codex 远程会话审批策略:"];
|
|
671
|
+
if (cfg.approval_policy) lines.push(` 策略 : ${cfg.approval_policy}`);
|
|
672
|
+
if (cfg.approvals_reviewer) lines.push(` 审批人 : ${cfg.approvals_reviewer}`);
|
|
673
|
+
lines.push("");
|
|
674
|
+
lines.push("策略说明:");
|
|
675
|
+
lines.push(" • on-request — codex 主动请求时才弹审批");
|
|
676
|
+
lines.push(" • on-failure — 命令失败时才请求审批");
|
|
677
|
+
lines.push(" • never — 不需要审批(全自动)");
|
|
678
|
+
lines.push(" • untrusted — 任何不受信操作都要审批");
|
|
679
|
+
lines.push("");
|
|
680
|
+
lines.push("修改: 编辑 ~/.codex/config.toml 的 approval_policy 后重启 bridge。");
|
|
681
|
+
return lines.join("\n");
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
"/mcp": async (ctx) => {
|
|
685
|
+
// 优先用 `codex mcp list` 真起 CLI 拿权威表格(列名: Name Command Args Env Cwd Status Auth)
|
|
686
|
+
const real = await execCliSafe("codex", ["mcp", "list"], { timeoutMs: 5000 });
|
|
687
|
+
if (real && real.length > 0) {
|
|
688
|
+
return [
|
|
689
|
+
"Codex MCP 服务器列表(实时, 来自 `codex mcp list`):",
|
|
690
|
+
"",
|
|
691
|
+
real,
|
|
692
|
+
"",
|
|
693
|
+
"管理: 在本机终端用 `codex mcp add <name> ...` / `codex mcp remove <name>`,",
|
|
694
|
+
"或编辑 ~/.codex/config.toml 的 [mcp_servers.<name>] 段后重启 bridge。",
|
|
695
|
+
].join("\n");
|
|
696
|
+
}
|
|
697
|
+
// 退回到 sessionConfigured(运行中 MCP 进程上报)
|
|
698
|
+
const cfg = ctx.sessionConfigured;
|
|
699
|
+
if (cfg?.mcp_servers) {
|
|
700
|
+
const servers = cfg.mcp_servers;
|
|
701
|
+
const lines = ["Codex 当前运行的 MCP 服务器(来自 session_configured):"];
|
|
702
|
+
if (Array.isArray(servers)) {
|
|
703
|
+
if (servers.length === 0) return "codex 当前会话没挂载任何外部 MCP server。";
|
|
704
|
+
for (const s of servers) {
|
|
705
|
+
if (typeof s === "string") lines.push(` • ${s}`);
|
|
706
|
+
else if (s && typeof s === "object") lines.push(` • ${s.name || JSON.stringify(s)}`);
|
|
707
|
+
}
|
|
708
|
+
return lines.join("\n");
|
|
709
|
+
} else if (typeof servers === "object") {
|
|
710
|
+
const keys = Object.keys(servers);
|
|
711
|
+
if (keys.length === 0) return "codex 当前会话没挂载任何外部 MCP server。";
|
|
712
|
+
for (const k of keys) lines.push(` • ${k}`);
|
|
713
|
+
return lines.join("\n");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return "codex 没挂载任何外部 MCP server, 也未与 codex MCP 建立 session。\n\n挂载: 本机执行 `codex mcp add <name> <cmd>` 或改 ~/.codex/config.toml。";
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
"/compact": () => [
|
|
720
|
+
"Codex 远程会话不支持 /compact:",
|
|
721
|
+
"",
|
|
722
|
+
" ▸ codex MCP 协议没有暴露 compaction API, bridge 拿不到压缩接口。",
|
|
723
|
+
" ▸ 上下文太长时, 请发 /clear 重新开始; 或在 App 聊天列表新建对话。",
|
|
724
|
+
" ▸ 想保留关键信息: 先让 codex 总结一下当前对话, 复制摘要, /clear 后",
|
|
725
|
+
" 把摘要作为第一条消息粘进去。",
|
|
726
|
+
].join("\n"),
|
|
727
|
+
|
|
728
|
+
"/init": () => [
|
|
729
|
+
"Codex /init(生成 AGENTS.md):",
|
|
730
|
+
"",
|
|
731
|
+
" ▸ 远程会话不能直接写本机文件(codex MCP 在受限沙箱里)。",
|
|
732
|
+
" ▸ 替代做法: 直接说「在 cwd 下生成一个 AGENTS.md, 描述项目结构 + 关键命令」,",
|
|
733
|
+
" codex 会用文件写入工具搞定; 写入路径受 file_system 白名单约束(/permissions 看)。",
|
|
734
|
+
" ▸ 想精准走交互模式 /init 流程: 本机终端 `codex` 进 TUI 后执行。",
|
|
735
|
+
].join("\n"),
|
|
736
|
+
|
|
737
|
+
"/diff": () => [
|
|
738
|
+
"Codex /diff:",
|
|
739
|
+
"",
|
|
740
|
+
" ▸ 远程会话不能直接显示 git diff UI。说「跑 git diff 把改动给我」,",
|
|
741
|
+
" codex 会调 bash 工具执行 git 命令并把结果返回(受 file_system 白名单约束)。",
|
|
742
|
+
" ▸ 想看 staged diff: 「跑 git diff --cached」; 想对比某 commit: 「跑 git diff HEAD~1」。",
|
|
743
|
+
" ▸ 本机 `codex apply` 子命令能把 codex 上轮 diff 直接 git apply 到工作树。",
|
|
744
|
+
].join("\n"),
|
|
745
|
+
|
|
746
|
+
"/review": () => [
|
|
747
|
+
"Codex 代码评审:",
|
|
748
|
+
"",
|
|
749
|
+
" ▸ 远程会话: 直接说「帮我 review 最近的 git diff」或具体到某文件,",
|
|
750
|
+
" codex 会读文件 / 跑 git 工具完成。",
|
|
751
|
+
" ▸ 本机也可用 `codex review` 子命令做整仓评审(非交互, 适合 CI)。",
|
|
752
|
+
].join("\n"),
|
|
753
|
+
|
|
754
|
+
"/resume": () => ({ __kind: "interrupt_session_pick" }),
|
|
755
|
+
|
|
756
|
+
"/fork": () => [
|
|
757
|
+
"Codex 分叉(fork)说明:",
|
|
758
|
+
"",
|
|
759
|
+
" ▸ 在 App 聊天列表「新建对话」= fork 出新 codex session, 上下文不带过去。",
|
|
760
|
+
" ▸ 想从某条历史消息分叉: 暂未直接支持, 可以复制那条消息文本到新对话作为开头。",
|
|
761
|
+
" ▸ 本机 `codex fork` 子命令能从磁盘 session 分叉(远程不可用)。",
|
|
762
|
+
].join("\n"),
|
|
763
|
+
|
|
764
|
+
"/plugins": async () => {
|
|
765
|
+
// codex 没有 `codex plugin list` 子命令(只有 marketplace),所以直接
|
|
766
|
+
// 解析 ~/.codex/config.toml 的 [plugins.<name>] 段拿真实状态。
|
|
767
|
+
const cfgPath = join(homedir(), ".codex", "config.toml");
|
|
768
|
+
let body;
|
|
769
|
+
try {
|
|
770
|
+
body = readFileSync(cfgPath, "utf8");
|
|
771
|
+
} catch (err) {
|
|
772
|
+
return [
|
|
773
|
+
"Codex 插件状态: 无法读取 ~/.codex/config.toml",
|
|
774
|
+
` 原因: ${err?.message || err}`,
|
|
775
|
+
"",
|
|
776
|
+
"如果你没用过 codex, 这是正常的(config 在首次登录时生成)。",
|
|
777
|
+
].join("\n");
|
|
778
|
+
}
|
|
779
|
+
// 找所有形如 [plugins."xxx"] 或 [plugins.xxx] 的段
|
|
780
|
+
const headerRe = /^\[plugins\.(?:"([^"]+)"|([\w.-]+))\]\s*$/gm;
|
|
781
|
+
const plugins = [];
|
|
782
|
+
let m;
|
|
783
|
+
while ((m = headerRe.exec(body)) !== null) {
|
|
784
|
+
const name = m[1] || m[2];
|
|
785
|
+
// 找紧跟在 header 后面的 enabled = true/false
|
|
786
|
+
const startIdx = m.index + m[0].length;
|
|
787
|
+
// 截取到下一个 [ 或文件末尾
|
|
788
|
+
const nextHeader = body.indexOf("\n[", startIdx);
|
|
789
|
+
const sectionBody = body.slice(startIdx, nextHeader < 0 ? body.length : nextHeader);
|
|
790
|
+
const enabledMatch = /^\s*enabled\s*=\s*(true|false)/m.exec(sectionBody);
|
|
791
|
+
plugins.push({
|
|
792
|
+
name,
|
|
793
|
+
enabled: enabledMatch ? enabledMatch[1] === "true" : null,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
if (plugins.length === 0) {
|
|
797
|
+
return [
|
|
798
|
+
"Codex 插件: 当前没有安装任何插件。",
|
|
799
|
+
"",
|
|
800
|
+
"安装方式(本机终端): `codex plugin marketplace add <owner/repo>` 添加 marketplace,",
|
|
801
|
+
"然后用 codex desktop app 或 TUI 启用具体插件。",
|
|
802
|
+
"",
|
|
803
|
+
`配置文件: ${cfgPath}(查找 [plugins.*] 段)`,
|
|
804
|
+
].join("\n");
|
|
805
|
+
}
|
|
806
|
+
const lines = [`Codex 插件列表(实时, 来自 ${cfgPath}):`, ""];
|
|
807
|
+
for (const p of plugins) {
|
|
808
|
+
const status = p.enabled === null ? "?" : (p.enabled ? "✓ enabled" : "✗ disabled");
|
|
809
|
+
lines.push(` ${status.padEnd(12)} ${p.name}`);
|
|
810
|
+
}
|
|
811
|
+
lines.push("");
|
|
812
|
+
lines.push("管理: codex desktop app 内启用/禁用, 或直接改 config.toml 后重启 bridge。");
|
|
813
|
+
return lines.join("\n");
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
"/agent": () => [
|
|
817
|
+
"Codex Agent 配置:",
|
|
818
|
+
"",
|
|
819
|
+
" ▸ codex 的 agent 行为由 ~/.codex/config.toml 控制(model / approval /",
|
|
820
|
+
" sandbox / reasoning_effort 等), bridge 启动时一次性载入。",
|
|
821
|
+
" ▸ 改后重启 bridge 生效。当前实际配置看 /status 和 /permissions。",
|
|
822
|
+
" ▸ 高级 agent profile(switch 不同任务的 model+sandbox 组合)走本机 codex TUI。",
|
|
823
|
+
].join("\n"),
|
|
824
|
+
|
|
825
|
+
"/apps": () => [
|
|
826
|
+
"Codex Apps:",
|
|
827
|
+
"",
|
|
828
|
+
" ▸ codex apps 是 OpenAI 生态里跨工具的应用容器,在 codex desktop app 里管理,",
|
|
829
|
+
" 远程会话(bridge 模式)看不到 apps 状态。",
|
|
830
|
+
" ▸ 本机执行 `codex app` 进 desktop app 入口。",
|
|
831
|
+
].join("\n"),
|
|
832
|
+
|
|
833
|
+
"/personality": () => [
|
|
834
|
+
"Codex Personality:",
|
|
835
|
+
"",
|
|
836
|
+
" ▸ personality 控制 codex 的回复风格(简洁/详尽/学究等),配置在 codex desktop",
|
|
837
|
+
" app 或 ~/.codex/config.toml。",
|
|
838
|
+
" ▸ 远程会话用当前配置, 想临时改风格直接说「以后回答都简短点」即可,",
|
|
839
|
+
" codex 会按指令调整, 等价于设了 personality。",
|
|
840
|
+
].join("\n"),
|
|
841
|
+
|
|
842
|
+
"/experimental": async () => {
|
|
843
|
+
// 真起 `codex features list` 拿权威列表(每行: <name> <stage> <enabled>)
|
|
844
|
+
const real = await execCliSafe("codex", ["features", "list"], { timeoutMs: 5000 });
|
|
845
|
+
if (real && real.length > 0) {
|
|
846
|
+
return [
|
|
847
|
+
"Codex 实验性特性(实时, 来自 `codex features list`):",
|
|
848
|
+
"",
|
|
849
|
+
real,
|
|
850
|
+
"",
|
|
851
|
+
"管理: `codex features enable <name>` / `codex features disable <name>`,",
|
|
852
|
+
"或编辑 ~/.codex/config.toml 的 [features] 段后重启 bridge。",
|
|
853
|
+
].join("\n");
|
|
854
|
+
}
|
|
855
|
+
return [
|
|
856
|
+
"Codex 实验性特性: 当前 codex CLI 未返回 feature 列表(可能未登录或 binary 版本不支持)。",
|
|
857
|
+
"",
|
|
858
|
+
"查看: 本机 `codex features list`。",
|
|
859
|
+
"管理: `codex features enable <name>` / `codex features disable <name>`,",
|
|
860
|
+
"或编辑 ~/.codex/config.toml 的 [features] 段后重启 bridge。",
|
|
861
|
+
].join("\n");
|
|
862
|
+
},
|
|
863
|
+
|
|
864
|
+
"/plan": () => [
|
|
865
|
+
"Codex Plan 模式:",
|
|
866
|
+
"",
|
|
867
|
+
" ▸ Plan 模式让 codex 先输出多步骤计划, 再逐步执行。远程会话 codex 在",
|
|
868
|
+
" multi-step 任务里会自动生成 turn/plan/updated 事件(App 顶部 todo 面板",
|
|
869
|
+
" 展示)。",
|
|
870
|
+
" ▸ 强制开 plan 模式: 直接说「先给我一个 plan, 不要立即动手」, codex 会按",
|
|
871
|
+
" plan 流程走。",
|
|
872
|
+
" ▸ 本机 TUI 的 /plan 切换按钮在远程没有 UI 入口, 用对话指令即可。",
|
|
873
|
+
].join("\n"),
|
|
874
|
+
|
|
875
|
+
"/fast": () => [
|
|
876
|
+
"Codex Fast 模式:",
|
|
877
|
+
"",
|
|
878
|
+
" ▸ Fast 模式降低 reasoning_effort 提速。当前 reasoning_effort 看 /status。",
|
|
879
|
+
" ▸ 想临时切到 fast: 直接说「这次任务用最快档,别想太多」, codex 会自动调整;",
|
|
880
|
+
" 或改 ~/.codex/config.toml 的 reasoning_effort = \"low\" 后重启 bridge。",
|
|
881
|
+
].join("\n"),
|
|
882
|
+
|
|
883
|
+
"/ps": () => [
|
|
884
|
+
"Codex 任务进程列表:",
|
|
885
|
+
"",
|
|
886
|
+
" ▸ codex 本机 TUI 的 /ps 列出后台跑的 task,远程会话 bridge 是单 turn 模型",
|
|
887
|
+
" (turn 完即返回), 没有后台 task 概念。",
|
|
888
|
+
" ▸ 想看 bridge 上跑过的 turn 历史: 滚动 App 聊天页向上看。",
|
|
889
|
+
" ▸ 想看 codex 本机历史 session: 本机 `codex resume` 或 `codex ls`。",
|
|
890
|
+
].join("\n"),
|
|
891
|
+
|
|
892
|
+
"/stop": () => [
|
|
893
|
+
"Codex /stop:",
|
|
894
|
+
"",
|
|
895
|
+
" ▸ 远程会话停止当前 run: App 聊天页输入框右侧停止按钮(streaming 时显示)。",
|
|
896
|
+
" ▸ 等价 codex 内部的 abort 事件, bridge 会把 turn 取消并清理。",
|
|
897
|
+
" ▸ 强制关掉整个 codex MCP 进程: 发 /clear, bridge 会 evict MCP 并下一条消息开新会话。",
|
|
898
|
+
].join("\n"),
|
|
899
|
+
|
|
900
|
+
"/mention": () => [
|
|
901
|
+
"Codex /mention(@ 引用):",
|
|
902
|
+
"",
|
|
903
|
+
" ▸ codex 本机 TUI 的 /mention 用于 @ 文件 / 符号, 给上下文加锚点。",
|
|
904
|
+
" ▸ 远程会话: 直接在消息里写文件路径(`src/foo.ts`) / 函数名, codex 会自动",
|
|
905
|
+
" 用 Read / Grep 工具去找。",
|
|
906
|
+
].join("\n"),
|
|
907
|
+
|
|
908
|
+
"/copy": () => [
|
|
909
|
+
"Codex /copy(复制):",
|
|
910
|
+
"",
|
|
911
|
+
" ▸ codex 本机 TUI 的 /copy 把上轮回复拷到剪贴板。",
|
|
912
|
+
" ▸ 远程会话: App 聊天页长按消息有「复制」选项。",
|
|
913
|
+
].join("\n"),
|
|
914
|
+
|
|
915
|
+
"/feedback": () => [
|
|
916
|
+
"提交 Codex / 本远程会话反馈:",
|
|
917
|
+
"",
|
|
918
|
+
" ▸ App「我的」→「意见反馈」入口提交, 会带上 codex 版本 + 当前 thread/run id,",
|
|
919
|
+
" 帮我们快速定位。",
|
|
920
|
+
" ▸ Codex 本体 bug 也可以去 https://github.com/openai/codex/issues 报。",
|
|
921
|
+
].join("\n"),
|
|
922
|
+
|
|
923
|
+
// 兜底
|
|
924
|
+
__default__: (_ctx, _args, command) => [
|
|
925
|
+
`「${command}」是 Codex CLI 的本机交互命令, 远程会话受 codex MCP 协议限制,`,
|
|
926
|
+
"没有等价的 UI 入口。",
|
|
927
|
+
"",
|
|
928
|
+
"替代做法:",
|
|
929
|
+
" ▸ 直接用自然语言告诉 codex 你想做什么(大部分 CLI 命令的功能都能通过对话完成,",
|
|
930
|
+
" 比如「跑 git status」「读 src/foo.ts 第 20 行」「装 ABC npm 包」)。",
|
|
931
|
+
" ▸ 系统级 / 配置类操作请去本机终端 `codex` 跑。",
|
|
932
|
+
].join("\n"),
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Codex /usage — 跟 claude /usage 对齐, 显示 5h(primary) + weekly(secondary) 配额。
|
|
937
|
+
* 数据源优先级:
|
|
938
|
+
* 1. ctx.rateLimits — 当前 thread 在 codex MCP 跑过至少一轮后捕获的最新 rate_limits
|
|
939
|
+
* (来自 codex/event msg.type=token_count, 跟 codex 跟 OpenAI 拉到的同一份)
|
|
940
|
+
* 2. ~/.codex/sessions/<最近 jsonl>/event_msg.token_count.rate_limits — fallback,
|
|
941
|
+
* bridge 刚启 / 当前 thread 还没跑过 turn 时, 读本机最近一次 codex 跑的会话
|
|
942
|
+
* jsonl 拿最新一条 rate_limits(可能稍旧, 但比"未知"强)
|
|
943
|
+
*/
|
|
944
|
+
function formatCodexUsage(ctx, command) {
|
|
945
|
+
let rl = ctx.rateLimits || null;
|
|
946
|
+
let source = "当前 thread 实时";
|
|
947
|
+
if (!rl) {
|
|
948
|
+
rl = readLatestCodexRateLimits();
|
|
949
|
+
if (rl) source = "本机最近一次 codex 会话(可能不是最新)";
|
|
950
|
+
}
|
|
951
|
+
if (!rl) {
|
|
952
|
+
return [
|
|
953
|
+
`Codex ${command}: 还没探到 rate_limits 数据。`,
|
|
954
|
+
"",
|
|
955
|
+
" ▸ 远程会话: 先在当前 thread 发一条普通消息让 codex MCP 跑一轮, 再发 /usage,",
|
|
956
|
+
" bridge 会从 codex/event token_count 抓 5h + weekly 额度。",
|
|
957
|
+
" ▸ 本机: 在终端跑 `codex` 进 TUI 跑一轮 turn 也行(会写 ~/.codex/sessions/),",
|
|
958
|
+
" bridge 下次 /usage 就能从最近 jsonl 里 fallback 读到。",
|
|
959
|
+
].join("\n");
|
|
960
|
+
}
|
|
961
|
+
const lines = [`Codex ${command}(实时, ${source}):`];
|
|
962
|
+
if (rl.plan_type) lines.push(` 套餐 : ${rl.plan_type}`);
|
|
963
|
+
if (rl.primary) {
|
|
964
|
+
const window = rl.primary.window_minutes;
|
|
965
|
+
const label = window === 300 ? "5h 滚动窗口 " : `${window} 分钟窗口`;
|
|
966
|
+
const pct = rl.primary.used_percent;
|
|
967
|
+
let line = ` ${label}: 用量 ${pct}%`;
|
|
968
|
+
if (rl.primary.resets_at) line += `, ${formatResetsRelative(rl.primary.resets_at)} 后重置`;
|
|
969
|
+
lines.push(line);
|
|
970
|
+
}
|
|
971
|
+
if (rl.secondary) {
|
|
972
|
+
const window = rl.secondary.window_minutes;
|
|
973
|
+
const label = window === 10080 ? "weekly 窗口 " : `${window} 分钟窗口`;
|
|
974
|
+
const pct = rl.secondary.used_percent;
|
|
975
|
+
let line = ` ${label}: 用量 ${pct}%`;
|
|
976
|
+
if (rl.secondary.resets_at) line += `, ${formatResetsRelative(rl.secondary.resets_at)} 后重置`;
|
|
977
|
+
lines.push(line);
|
|
978
|
+
}
|
|
979
|
+
if (rl.credits) {
|
|
980
|
+
if (rl.credits.unlimited) lines.push(" 额外信息 : 无限额度");
|
|
981
|
+
else if (rl.credits.has_credits) lines.push(` 额外信息 : 有 credit 余额 ${rl.credits.balance ?? "未知"}`);
|
|
982
|
+
}
|
|
983
|
+
if (rl.rate_limit_reached_type) lines.push(` 已触达限制 : ${rl.rate_limit_reached_type}`);
|
|
984
|
+
if (rl.captured_at) lines.push(` 数据采集时间 : ${rl.captured_at}`);
|
|
985
|
+
lines.push("");
|
|
986
|
+
lines.push("(数据来自 codex MCP 的 token_count 事件, 跟 codex 自己跟 OpenAI 拉到的是同一份。");
|
|
987
|
+
lines.push(" 精确明细: console.openai.com/usage 网页查。)");
|
|
988
|
+
return lines.join("\n");
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* 读 ~/.codex/sessions/ 下最近一条 jsonl, 从最后往前找 event_msg.token_count.rate_limits。
|
|
993
|
+
* Fallback 用, 在 bridge 没活体捕获时给个"虽然旧但真"的数据。
|
|
994
|
+
*/
|
|
995
|
+
function readLatestCodexRateLimits() {
|
|
996
|
+
try {
|
|
997
|
+
const sessionsDir = join(homedir(), ".codex", "sessions");
|
|
998
|
+
// 按时间戳新到旧扫(目录格式: yyyy/mm/dd/), 一次只读一份
|
|
999
|
+
const newestFile = findNewestCodexSession(sessionsDir);
|
|
1000
|
+
if (!newestFile) return null;
|
|
1001
|
+
const body = readFileSync(newestFile, "utf8");
|
|
1002
|
+
const lines = body.split("\n");
|
|
1003
|
+
// 从后往前找 token_count
|
|
1004
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1005
|
+
const line = lines[i].trim();
|
|
1006
|
+
if (!line) continue;
|
|
1007
|
+
if (!line.includes('"rate_limits"')) continue;
|
|
1008
|
+
try {
|
|
1009
|
+
const obj = JSON.parse(line);
|
|
1010
|
+
const payload = obj?.payload;
|
|
1011
|
+
if (payload?.type === "token_count" && payload.rate_limits) {
|
|
1012
|
+
return {
|
|
1013
|
+
...payload.rate_limits,
|
|
1014
|
+
captured_at: obj.timestamp || null,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
} catch { /* skip */ }
|
|
1018
|
+
}
|
|
1019
|
+
return null;
|
|
1020
|
+
} catch {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function findNewestCodexSession(rootDir) {
|
|
1026
|
+
try {
|
|
1027
|
+
// 递归到最深一级取 mtime 最新的 .jsonl
|
|
1028
|
+
const stack = [rootDir];
|
|
1029
|
+
let best = null;
|
|
1030
|
+
let bestMtime = 0;
|
|
1031
|
+
while (stack.length) {
|
|
1032
|
+
const dir = stack.pop();
|
|
1033
|
+
let entries;
|
|
1034
|
+
try { entries = readdirSync(dir); } catch { continue; }
|
|
1035
|
+
for (const name of entries) {
|
|
1036
|
+
const full = join(dir, name);
|
|
1037
|
+
let st;
|
|
1038
|
+
try { st = statSync(full); } catch { continue; }
|
|
1039
|
+
if (st.isDirectory()) {
|
|
1040
|
+
stack.push(full);
|
|
1041
|
+
} else if (name.endsWith(".jsonl")) {
|
|
1042
|
+
const mt = st.mtimeMs;
|
|
1043
|
+
if (mt > bestMtime) { bestMtime = mt; best = full; }
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return best;
|
|
1048
|
+
} catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* 解析 ~/.codex/auth.json 的 id_token JWT, 拿订阅状态 / 计费方式 / 套餐类型。
|
|
1055
|
+
* 真实数据(codex 自己读这份文件判 plan), 不是合成模板。
|
|
1056
|
+
*/
|
|
1057
|
+
function readCodexAuthCost(command) {
|
|
1058
|
+
const authPath = join(homedir(), ".codex", "auth.json");
|
|
1059
|
+
let auth;
|
|
1060
|
+
try {
|
|
1061
|
+
auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
return [
|
|
1064
|
+
`Codex ${command}: 无法读取 ${authPath}`,
|
|
1065
|
+
` 原因: ${err?.message || err}`,
|
|
1066
|
+
"",
|
|
1067
|
+
"如果你没登录 codex, 先在本机跑 `codex login`。",
|
|
1068
|
+
].join("\n");
|
|
1069
|
+
}
|
|
1070
|
+
const lines = [`Codex ${command}(实时, 来自 ~/.codex/auth.json):`];
|
|
1071
|
+
if (auth.auth_mode) lines.push(` ▸ 认证方式 : ${auth.auth_mode}`);
|
|
1072
|
+
// 解 JWT id_token 拿 chatgpt_plan_type / subscription_active_until / email
|
|
1073
|
+
const idToken = auth?.tokens?.id_token;
|
|
1074
|
+
if (typeof idToken === "string") {
|
|
1075
|
+
const payload = decodeJwtPayload(idToken);
|
|
1076
|
+
const chatgpt = payload && payload["https://api.openai.com/auth"];
|
|
1077
|
+
if (chatgpt) {
|
|
1078
|
+
if (chatgpt.chatgpt_plan_type) lines.push(` ▸ ChatGPT 套餐 : ${chatgpt.chatgpt_plan_type}`);
|
|
1079
|
+
if (chatgpt.chatgpt_subscription_active_until) {
|
|
1080
|
+
const until = chatgpt.chatgpt_subscription_active_until;
|
|
1081
|
+
lines.push(` ▸ 订阅到期 : ${until}`);
|
|
1082
|
+
}
|
|
1083
|
+
if (chatgpt.chatgpt_subscription_last_checked) {
|
|
1084
|
+
lines.push(` ▸ 上次校验时间 : ${chatgpt.chatgpt_subscription_last_checked}`);
|
|
1085
|
+
}
|
|
1086
|
+
if (Array.isArray(chatgpt.organizations) && chatgpt.organizations.length > 0) {
|
|
1087
|
+
const org = chatgpt.organizations[0];
|
|
1088
|
+
lines.push(` ▸ 组织 / 角色 : ${org.title || org.id} / ${org.role || "?"}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (payload?.email) lines.push(` ▸ 登录账号 : ${payload.email}`);
|
|
1092
|
+
}
|
|
1093
|
+
if (auth.last_refresh) lines.push(` ▸ 令牌刷新时间 : ${auth.last_refresh}`);
|
|
1094
|
+
lines.push("");
|
|
1095
|
+
lines.push("说明: codex MCP 协议没暴露按 turn 的 token / USD 明细, 这里给的是");
|
|
1096
|
+
lines.push("订阅 / 计费方式总览 —— 跟 codex CLI 自己读的是同一份 auth.json。");
|
|
1097
|
+
lines.push("精确明细: console.openai.com/usage (网页登录后看)。");
|
|
1098
|
+
return lines.join("\n");
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* 解 JWT 中间段(payload), 不验签, 仅用于读取声明字段。失败返回 null。
|
|
1103
|
+
*/
|
|
1104
|
+
function decodeJwtPayload(token) {
|
|
1105
|
+
try {
|
|
1106
|
+
const parts = token.split(".");
|
|
1107
|
+
if (parts.length < 2) return null;
|
|
1108
|
+
// base64url → base64
|
|
1109
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
1110
|
+
// 补 padding
|
|
1111
|
+
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
|
1112
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
1113
|
+
} catch {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1119
|
+
// Hermes synth — 仅覆盖 bridge 真有数据的命令
|
|
1120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1121
|
+
//
|
|
1122
|
+
// Bridge 状态可用:
|
|
1123
|
+
// ctx.model pre.model
|
|
1124
|
+
// ctx.apiBaseUrl pre.apiBaseUrl (LLM endpoint)
|
|
1125
|
+
// ctx.backend "http"/"command"/"responses"
|
|
1126
|
+
// ctx.cwd, ctx.gwId, ctx.threadId
|
|
1127
|
+
// ctx.historyFn async () => [{user, assistant}, ...] (从 sessionStore)
|
|
1128
|
+
// ctx.usageStats { tokens, cost, turns } (从 usageReporter)
|
|
1129
|
+
//
|
|
1130
|
+
// 不支持的命令(/undo /retry /branch /title /steer /queue /yolo /reasoning /fast
|
|
1131
|
+
// /verbose /personality /compress /sessions /save /reload-* /tools /skills /agents)
|
|
1132
|
+
// 走 __default__ 兜底, 解释为什么远程拿不到 + 怎么替代。
|
|
1133
|
+
async function synthesizeHermesSlashResponse(command, args, ctx) {
|
|
1134
|
+
const handler = HERMES_HANDLERS[command];
|
|
1135
|
+
if (handler) return await handler(ctx, args, command);
|
|
1136
|
+
return await HERMES_HANDLERS.__default__(ctx, args, command);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const HERMES_HANDLERS = {
|
|
1140
|
+
"/help": (ctx) => [
|
|
1141
|
+
"Hermes 远程会话支持的系统指令(bridge 有真实数据可合成):",
|
|
1142
|
+
"",
|
|
1143
|
+
" /clear /new 清除当前对话上下文",
|
|
1144
|
+
" /status 会话状态(model/cwd/backend/history 长度)",
|
|
1145
|
+
" /usage token / cost 用量(由 bridge usageReporter 累计)",
|
|
1146
|
+
" /model 当前模型 + LLM endpoint",
|
|
1147
|
+
" /version bridge 版本",
|
|
1148
|
+
" /history 本会话历史 turn 列表",
|
|
1149
|
+
" /save 导出会话历史到 JSON",
|
|
1150
|
+
" /stop 停止当前回合(用 App 输入框旁边的停止键即可)",
|
|
1151
|
+
" /help 显示本帮助",
|
|
1152
|
+
"",
|
|
1153
|
+
"Hermes 其它官方命令(/undo /retry /branch /rollback /steer /queue /title",
|
|
1154
|
+
"/sessions /personality /reasoning /fast /verbose /yolo /compress /reload-*",
|
|
1155
|
+
"/tools /skills /agents) 远程会话拿不到对应数据, 在本机 `hermes` TUI 用。",
|
|
1156
|
+
].join("\n"),
|
|
1157
|
+
|
|
1158
|
+
"/status": async (ctx) => {
|
|
1159
|
+
const lines = ["Hermes 远程会话状态:"];
|
|
1160
|
+
if (ctx.model) lines.push(` 当前模型 : ${ctx.model}`);
|
|
1161
|
+
if (ctx.apiBaseUrl) lines.push(` LLM endpoint : ${ctx.apiBaseUrl}`);
|
|
1162
|
+
if (ctx.backend) lines.push(` backend : ${ctx.backend}`);
|
|
1163
|
+
if (ctx.cwd) lines.push(` cwd : ${ctx.cwd}`);
|
|
1164
|
+
if (ctx.gwId) lines.push(` gateway id : ${ctx.gwId}`);
|
|
1165
|
+
if (ctx.threadId) lines.push(` thread id : ${ctx.threadId}`);
|
|
1166
|
+
if (typeof ctx.historyFn === "function") {
|
|
1167
|
+
try {
|
|
1168
|
+
const hist = await ctx.historyFn();
|
|
1169
|
+
const turns = Array.isArray(hist) ? hist.length / 2 : 0;
|
|
1170
|
+
lines.push(` 历史 turn 数 : ${Math.floor(turns)}`);
|
|
1171
|
+
} catch { /* ignore */ }
|
|
1172
|
+
}
|
|
1173
|
+
return lines.join("\n");
|
|
1174
|
+
},
|
|
1175
|
+
|
|
1176
|
+
"/usage": (ctx) => {
|
|
1177
|
+
const u = ctx.usageStats;
|
|
1178
|
+
const lines = ["Hermes 远程会话用量(bridge 累计):"];
|
|
1179
|
+
if (u?.tokens) {
|
|
1180
|
+
lines.push(` 输入 token : ${u.tokens.input ?? 0}`);
|
|
1181
|
+
lines.push(` 输出 token : ${u.tokens.output ?? 0}`);
|
|
1182
|
+
lines.push(` 缓存 input : ${u.tokens.cached_input ?? 0}`);
|
|
1183
|
+
lines.push(` 推理 output : ${u.tokens.reasoning_output ?? 0}`);
|
|
1184
|
+
lines.push(` 累计 total : ${u.tokens.total ?? 0}`);
|
|
1185
|
+
} else {
|
|
1186
|
+
lines.push(" 本次 bridge 启动后还没积累 token 数据。");
|
|
1187
|
+
}
|
|
1188
|
+
if (u?.cost) {
|
|
1189
|
+
if (u.cost.total_usd) lines.push(` 累计 USD : $${Number(u.cost.total_usd).toFixed(4)}`);
|
|
1190
|
+
if (u.cost.turns) lines.push(` 累计 turn 数 : ${u.cost.turns}`);
|
|
1191
|
+
if (u.cost.duration_ms) lines.push(` 累计耗时(ms) : ${u.cost.duration_ms}`);
|
|
1192
|
+
}
|
|
1193
|
+
lines.push("");
|
|
1194
|
+
lines.push("(注意: bridge 重启会清零, 这只是当前进程内的累计。");
|
|
1195
|
+
lines.push(" 账户级总用量请去对应 LLM provider 后台查。)");
|
|
1196
|
+
return lines.join("\n");
|
|
1197
|
+
},
|
|
1198
|
+
|
|
1199
|
+
"/model": (ctx) => {
|
|
1200
|
+
const lines = ["Hermes 当前模型配置:"];
|
|
1201
|
+
if (ctx.model) lines.push(` 模型 id : ${ctx.model}`);
|
|
1202
|
+
if (ctx.apiBaseUrl) lines.push(` LLM endpoint : ${ctx.apiBaseUrl}`);
|
|
1203
|
+
if (ctx.backend) lines.push(` backend 类型 : ${ctx.backend}`);
|
|
1204
|
+
lines.push("");
|
|
1205
|
+
lines.push("切换模型: 编辑 ~/.hermes/.env 的 HERMES_BRIDGE_MODEL 字段后重启 bridge,");
|
|
1206
|
+
lines.push("或用 App 顶部「模型」入口(如果支持)。");
|
|
1207
|
+
return lines.join("\n");
|
|
1208
|
+
},
|
|
1209
|
+
|
|
1210
|
+
"/version": (ctx) => {
|
|
1211
|
+
return ctx.version
|
|
1212
|
+
? `Hermes bridge 版本: ${ctx.version}`
|
|
1213
|
+
: "Hermes bridge 版本未知(bridge 未上报)";
|
|
1214
|
+
},
|
|
1215
|
+
|
|
1216
|
+
"/history": async (ctx) => {
|
|
1217
|
+
if (typeof ctx.historyFn !== "function") {
|
|
1218
|
+
return "Hermes 会话历史: bridge 未注入 historyFn, 无法读取。";
|
|
1219
|
+
}
|
|
1220
|
+
let hist;
|
|
1221
|
+
try { hist = await ctx.historyFn(); } catch (err) {
|
|
1222
|
+
return `Hermes 会话历史: 读取失败 — ${err?.message || err}`;
|
|
1223
|
+
}
|
|
1224
|
+
if (!Array.isArray(hist) || hist.length === 0) {
|
|
1225
|
+
return "Hermes 会话历史: 当前还没有任何 turn(本 bridge 启动后)。";
|
|
1226
|
+
}
|
|
1227
|
+
const lines = [`Hermes 会话历史(${Math.floor(hist.length / 2)} 个 turn):`, ""];
|
|
1228
|
+
let turnIdx = 0;
|
|
1229
|
+
for (let i = 0; i < hist.length; i++) {
|
|
1230
|
+
const m = hist[i];
|
|
1231
|
+
const role = m?.role;
|
|
1232
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
1233
|
+
if (role === "user") turnIdx += 1;
|
|
1234
|
+
const tag = role === "user" ? `[User #${turnIdx}]` : `[Hermes #${turnIdx}]`;
|
|
1235
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
1236
|
+
const preview = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
1237
|
+
lines.push(`${tag} ${preview}`);
|
|
1238
|
+
if (lines.length > 40) { lines.push("…(更多 turn 省略)"); break; }
|
|
1239
|
+
}
|
|
1240
|
+
return lines.join("\n");
|
|
1241
|
+
},
|
|
1242
|
+
|
|
1243
|
+
"/save": async (ctx) => {
|
|
1244
|
+
if (typeof ctx.historyFn !== "function") {
|
|
1245
|
+
return "Hermes /save: bridge 没注入 historyFn, 无法导出。";
|
|
1246
|
+
}
|
|
1247
|
+
let hist;
|
|
1248
|
+
try { hist = await ctx.historyFn(); } catch (err) {
|
|
1249
|
+
return `Hermes /save 失败: ${err?.message || err}`;
|
|
1250
|
+
}
|
|
1251
|
+
if (!Array.isArray(hist) || hist.length === 0) {
|
|
1252
|
+
return "Hermes /save: 当前会话还没有任何 turn 可保存。";
|
|
1253
|
+
}
|
|
1254
|
+
// 写到 /tmp/hermes-session-<gwId>-<timestamp>.json
|
|
1255
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1256
|
+
const fname = `/tmp/hermes-session-${ctx.gwId || "unknown"}-${ts}.json`;
|
|
1257
|
+
try {
|
|
1258
|
+
const fs = await import("node:fs/promises");
|
|
1259
|
+
await fs.writeFile(fname, JSON.stringify({
|
|
1260
|
+
gw_id: ctx.gwId, thread_id: ctx.threadId, model: ctx.model,
|
|
1261
|
+
saved_at: new Date().toISOString(), history: hist,
|
|
1262
|
+
}, null, 2), "utf8");
|
|
1263
|
+
return [
|
|
1264
|
+
`Hermes 会话已导出到本机:`,
|
|
1265
|
+
` ${fname}`,
|
|
1266
|
+
` 共 ${Math.floor(hist.length / 2)} 个 turn`,
|
|
1267
|
+
].join("\n");
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
return `Hermes /save: 写文件失败 — ${err?.message || err}`;
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
|
|
1273
|
+
"/resume": () => ({ __kind: "interrupt_session_pick" }),
|
|
1274
|
+
|
|
1275
|
+
"/stop": () => [
|
|
1276
|
+
"Hermes /stop: 远程会话停止当前回合的方式 —",
|
|
1277
|
+
"",
|
|
1278
|
+
" ▸ App 聊天页输入框右侧, AI 在打字时会显示「停止」按钮, 点它即可。",
|
|
1279
|
+
" ▸ bridge 收到停止信号会取消当前 LLM 请求并清理。",
|
|
1280
|
+
" ▸ 在对话里发 /stop 文本本身不停 — 因为消息要先发出去才会被读到, 那时回合",
|
|
1281
|
+
" 已经在跑了。",
|
|
1282
|
+
].join("\n"),
|
|
1283
|
+
|
|
1284
|
+
// 兜底: 不支持的命令简短说明 + 替代做法
|
|
1285
|
+
__default__: (_ctx, _args, command) => [
|
|
1286
|
+
`「${command}」远程 Hermes 会话没有等价行为(bridge 是 thin LLM wrapper,`,
|
|
1287
|
+
"没有 hermes-agent 的会话状态机)。",
|
|
1288
|
+
"",
|
|
1289
|
+
"想要这种功能, 在装了 `hermes` 的本机终端跑该命令。",
|
|
1290
|
+
"远程可用的命令: /clear /new /status /usage /model /version /history /save",
|
|
1291
|
+
"/stop /help。",
|
|
1292
|
+
].join("\n"),
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
function formatResetsRelative(resetsAt) {
|
|
1296
|
+
if (!Number.isFinite(resetsAt)) return "(未知时间)";
|
|
1297
|
+
const diffMs = (resetsAt * 1000) - Date.now();
|
|
1298
|
+
if (diffMs <= 0) return "已重置";
|
|
1299
|
+
const mins = Math.round(diffMs / 60000);
|
|
1300
|
+
if (mins < 60) return `${mins} 分钟`;
|
|
1301
|
+
const h = Math.floor(mins / 60), m = mins % 60;
|
|
1302
|
+
return m === 0 ? `${h} 小时` : `${h} 小时 ${m} 分钟`;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function formatCodexPermissions(cfg) {
|
|
1306
|
+
if (!cfg) return "权限/沙箱配置未知 — 尚未与 codex MCP 建立 session。先发一条普通消息触发 codex 启动, 然后再发 /permissions 即可看到真实配置。";
|
|
1307
|
+
const lines = ["Codex 远程会话权限 / 沙箱:"];
|
|
1308
|
+
const ap = cfg.active_permission_profile;
|
|
1309
|
+
if (ap?.id) lines.push(` 激活 profile : ${ap.id}`);
|
|
1310
|
+
const pp = cfg.permission_profile;
|
|
1311
|
+
if (pp?.type) lines.push(` permission 类型 : ${pp.type}`);
|
|
1312
|
+
if (pp?.file_system) {
|
|
1313
|
+
lines.push(` 文件系统:`);
|
|
1314
|
+
const fs = pp.file_system;
|
|
1315
|
+
if (fs.type) lines.push(` 访问模式 : ${fs.type}`);
|
|
1316
|
+
const entries = Array.isArray(fs.entries) ? fs.entries : [];
|
|
1317
|
+
if (entries.length > 0) {
|
|
1318
|
+
lines.push(` 白名单(${entries.length} 条):`);
|
|
1319
|
+
for (const e of entries.slice(0, 10)) {
|
|
1320
|
+
const p = e?.path;
|
|
1321
|
+
const pathDisp = p?.type === "special" ? `<${p.value?.kind || "?"}>` : (p?.value || String(p || "?"));
|
|
1322
|
+
lines.push(` - ${pathDisp} (${e?.access || "?"})`);
|
|
1323
|
+
}
|
|
1324
|
+
if (entries.length > 10) lines.push(` ... 还有 ${entries.length - 10} 条`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (pp?.network) lines.push(` 网络访问 : ${pp.network}`);
|
|
1328
|
+
if (cfg.approval_policy) lines.push(` 审批策略 : ${cfg.approval_policy}`);
|
|
1329
|
+
if (cfg.approvals_reviewer) lines.push(` 审批人 : ${cfg.approvals_reviewer}`);
|
|
1330
|
+
lines.push("");
|
|
1331
|
+
lines.push("(数据由 bridge 在 codex MCP 启动时从 `session_configured` 事件读取,");
|
|
1332
|
+
lines.push(" 是 codex CLI 真实活体状态, 不是合成模板。)");
|
|
1333
|
+
return lines.join("\n");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* 给 handleRequest 的 onChunk 用的便捷封装。
|
|
1338
|
+
*/
|
|
1339
|
+
export async function emitAssistantText(onChunk, text) {
|
|
1340
|
+
if (!text) {
|
|
1341
|
+
await onChunk({ event: "response.completed" });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
await onChunk({ event: "response.output_text.delta", delta: text });
|
|
1345
|
+
await onChunk({ event: "response.completed" });
|
|
1346
|
+
}
|