@akon-labs/nexus-cli 0.1.9 → 0.1.11-rc.2
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/CHANGELOG.md +21 -0
- package/LICENSE +1 -0
- package/README.md +3 -2
- package/dist/app-bridge.bundle.js +67 -0
- package/dist/cli/require-login.d.ts.map +1 -1
- package/dist/cli/require-login.js +2 -2
- package/dist/cli/require-login.js.map +1 -1
- package/dist/cli.js +20866 -1882
- package/dist/core/agent-session.d.ts +3 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +5 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auto-model.d.ts +1 -1
- package/dist/core/auto-model.d.ts.map +1 -1
- package/dist/core/auto-model.js +1 -1
- package/dist/core/auto-model.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +7 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +14 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +18 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts +2 -2
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +3 -3
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +15 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +50 -0
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +3 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +5 -5
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/websearch.d.ts.map +1 -1
- package/dist/core/tools/websearch.js +3 -3
- package/dist/core/tools/websearch.js.map +1 -1
- package/dist/extensions/gitnexus/config.d.ts +15 -1
- package/dist/extensions/gitnexus/config.d.ts.map +1 -1
- package/dist/extensions/gitnexus/config.js +86 -2
- package/dist/extensions/gitnexus/config.js.map +1 -1
- package/dist/extensions/gitnexus/gateway.d.ts +6 -6
- package/dist/extensions/gitnexus/gateway.d.ts.map +1 -1
- package/dist/extensions/gitnexus/gateway.js +4 -4
- package/dist/extensions/gitnexus/gateway.js.map +1 -1
- package/dist/extensions/gitnexus/index.d.ts.map +1 -1
- package/dist/extensions/gitnexus/index.js +6 -4
- package/dist/extensions/gitnexus/index.js.map +1 -1
- package/dist/extensions/gitnexus/observatory.d.ts.map +1 -1
- package/dist/extensions/gitnexus/observatory.js +2 -2
- package/dist/extensions/gitnexus/observatory.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +3 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/mcp/abort.d.ts +3 -0
- package/dist/extensions/mcp/abort.d.ts.map +1 -0
- package/dist/extensions/mcp/abort.js +41 -0
- package/dist/extensions/mcp/abort.js.map +1 -0
- package/dist/extensions/mcp/agent-dir.d.ts +6 -0
- package/dist/extensions/mcp/agent-dir.d.ts.map +1 -0
- package/dist/extensions/mcp/agent-dir.js +18 -0
- package/dist/extensions/mcp/agent-dir.js.map +1 -0
- package/dist/extensions/mcp/agent-plugin-loader.d.ts +9 -0
- package/dist/extensions/mcp/agent-plugin-loader.d.ts.map +1 -0
- package/dist/extensions/mcp/agent-plugin-loader.js +384 -0
- package/dist/extensions/mcp/agent-plugin-loader.js.map +1 -0
- package/dist/extensions/mcp/app-bridge.bundle.js +67 -0
- package/dist/extensions/mcp/commands.d.ts +47 -0
- package/dist/extensions/mcp/commands.d.ts.map +1 -0
- package/dist/extensions/mcp/commands.js +821 -0
- package/dist/extensions/mcp/commands.js.map +1 -0
- package/dist/extensions/mcp/config.d.ts +123 -38
- package/dist/extensions/mcp/config.d.ts.map +1 -1
- package/dist/extensions/mcp/config.js +1035 -70
- package/dist/extensions/mcp/config.js.map +1 -1
- package/dist/extensions/mcp/consent-manager.d.ts +14 -0
- package/dist/extensions/mcp/consent-manager.d.ts.map +1 -0
- package/dist/extensions/mcp/consent-manager.js +60 -0
- package/dist/extensions/mcp/consent-manager.js.map +1 -0
- package/dist/extensions/mcp/direct-tools.d.ts +11 -0
- package/dist/extensions/mcp/direct-tools.d.ts.map +1 -0
- package/dist/extensions/mcp/direct-tools.js +562 -0
- package/dist/extensions/mcp/direct-tools.js.map +1 -0
- package/dist/extensions/mcp/elicitation-handler.d.ts +18 -0
- package/dist/extensions/mcp/elicitation-handler.d.ts.map +1 -0
- package/dist/extensions/mcp/elicitation-handler.js +320 -0
- package/dist/extensions/mcp/elicitation-handler.js.map +1 -0
- package/dist/extensions/mcp/error-signal.d.ts +16 -0
- package/dist/extensions/mcp/error-signal.d.ts.map +1 -0
- package/dist/extensions/mcp/error-signal.js +22 -0
- package/dist/extensions/mcp/error-signal.js.map +1 -0
- package/dist/extensions/mcp/errors.d.ts +104 -0
- package/dist/extensions/mcp/errors.d.ts.map +1 -0
- package/dist/extensions/mcp/errors.js +171 -0
- package/dist/extensions/mcp/errors.js.map +1 -0
- package/dist/extensions/mcp/glimpse-ui.d.ts +10 -0
- package/dist/extensions/mcp/glimpse-ui.d.ts.map +1 -0
- package/dist/extensions/mcp/glimpse-ui.js +67 -0
- package/dist/extensions/mcp/glimpse-ui.js.map +1 -0
- package/dist/extensions/mcp/host-html-template.d.ts +17 -0
- package/dist/extensions/mcp/host-html-template.d.ts.map +1 -0
- package/dist/extensions/mcp/host-html-template.js +442 -0
- package/dist/extensions/mcp/host-html-template.js.map +1 -0
- package/dist/extensions/mcp/index.d.ts +20 -15
- package/dist/extensions/mcp/index.d.ts.map +1 -1
- package/dist/extensions/mcp/index.js +1113 -82
- package/dist/extensions/mcp/index.js.map +1 -1
- package/dist/extensions/mcp/init.d.ts +28 -0
- package/dist/extensions/mcp/init.d.ts.map +1 -0
- package/dist/extensions/mcp/init.js +631 -0
- package/dist/extensions/mcp/init.js.map +1 -0
- package/dist/extensions/mcp/json-schema-validator.d.ts +3 -0
- package/dist/extensions/mcp/json-schema-validator.d.ts.map +1 -0
- package/dist/extensions/mcp/json-schema-validator.js +53 -0
- package/dist/extensions/mcp/json-schema-validator.js.map +1 -0
- package/dist/extensions/mcp/lifecycle.d.ts +56 -0
- package/dist/extensions/mcp/lifecycle.d.ts.map +1 -0
- package/dist/extensions/mcp/lifecycle.js +395 -0
- package/dist/extensions/mcp/lifecycle.js.map +1 -0
- package/dist/extensions/mcp/logger.d.ts +52 -0
- package/dist/extensions/mcp/logger.d.ts.map +1 -0
- package/dist/extensions/mcp/logger.js +134 -0
- package/dist/extensions/mcp/logger.js.map +1 -0
- package/dist/extensions/mcp/mcp-auth-flow.d.ts +117 -0
- package/dist/extensions/mcp/mcp-auth-flow.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-auth-flow.js +869 -0
- package/dist/extensions/mcp/mcp-auth-flow.js.map +1 -0
- package/dist/extensions/mcp/mcp-auth.d.ts +162 -0
- package/dist/extensions/mcp/mcp-auth.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-auth.js +897 -0
- package/dist/extensions/mcp/mcp-auth.js.map +1 -0
- package/dist/extensions/mcp/mcp-bearer-store.d.ts +25 -0
- package/dist/extensions/mcp/mcp-bearer-store.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-bearer-store.js +350 -0
- package/dist/extensions/mcp/mcp-bearer-store.js.map +1 -0
- package/dist/extensions/mcp/mcp-callback-server.d.ts +54 -0
- package/dist/extensions/mcp/mcp-callback-server.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-callback-server.js +454 -0
- package/dist/extensions/mcp/mcp-callback-server.js.map +1 -0
- package/dist/extensions/mcp/mcp-code.d.ts +48 -0
- package/dist/extensions/mcp/mcp-code.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-code.js +358 -0
- package/dist/extensions/mcp/mcp-code.js.map +1 -0
- package/dist/extensions/mcp/mcp-keyring-helper.cjs +89 -0
- package/dist/extensions/mcp/mcp-oauth-provider.d.ts +135 -0
- package/dist/extensions/mcp/mcp-oauth-provider.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-oauth-provider.js +491 -0
- package/dist/extensions/mcp/mcp-oauth-provider.js.map +1 -0
- package/dist/extensions/mcp/mcp-output-guard.d.ts +59 -0
- package/dist/extensions/mcp/mcp-output-guard.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-output-guard.js +343 -0
- package/dist/extensions/mcp/mcp-output-guard.js.map +1 -0
- package/dist/extensions/mcp/mcp-panel.d.ts +71 -0
- package/dist/extensions/mcp/mcp-panel.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-panel.js +973 -0
- package/dist/extensions/mcp/mcp-panel.js.map +1 -0
- package/dist/extensions/mcp/mcp-probe.d.ts +7 -0
- package/dist/extensions/mcp/mcp-probe.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-probe.js +155 -0
- package/dist/extensions/mcp/mcp-probe.js.map +1 -0
- package/dist/extensions/mcp/mcp-script-worker.mjs +130 -0
- package/dist/extensions/mcp/mcp-setup-panel.d.ts +146 -0
- package/dist/extensions/mcp/mcp-setup-panel.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-setup-panel.js +1104 -0
- package/dist/extensions/mcp/mcp-setup-panel.js.map +1 -0
- package/dist/extensions/mcp/mcp-status.d.ts +12 -0
- package/dist/extensions/mcp/mcp-status.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-status.js +98 -0
- package/dist/extensions/mcp/mcp-status.js.map +1 -0
- package/dist/extensions/mcp/mcp-trace.d.ts +95 -0
- package/dist/extensions/mcp/mcp-trace.d.ts.map +1 -0
- package/dist/extensions/mcp/mcp-trace.js +252 -0
- package/dist/extensions/mcp/mcp-trace.js.map +1 -0
- package/dist/extensions/mcp/metadata-cache.d.ts +20 -0
- package/dist/extensions/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/extensions/mcp/metadata-cache.js +318 -0
- package/dist/extensions/mcp/metadata-cache.js.map +1 -0
- package/dist/extensions/mcp/npx-resolver.d.ts +7 -0
- package/dist/extensions/mcp/npx-resolver.d.ts.map +1 -0
- package/dist/extensions/mcp/npx-resolver.js +505 -0
- package/dist/extensions/mcp/npx-resolver.js.map +1 -0
- package/dist/extensions/mcp/oauth.d.ts +21 -0
- package/dist/extensions/mcp/oauth.d.ts.map +1 -0
- package/dist/extensions/mcp/oauth.js +15 -0
- package/dist/extensions/mcp/oauth.js.map +1 -0
- package/dist/extensions/mcp/onboarding-state.d.ts +13 -0
- package/dist/extensions/mcp/onboarding-state.d.ts.map +1 -0
- package/dist/extensions/mcp/onboarding-state.js +65 -0
- package/dist/extensions/mcp/onboarding-state.js.map +1 -0
- package/dist/extensions/mcp/package-mcp-loader.d.ts +3 -0
- package/dist/extensions/mcp/package-mcp-loader.d.ts.map +1 -0
- package/dist/extensions/mcp/package-mcp-loader.js +170 -0
- package/dist/extensions/mcp/package-mcp-loader.js.map +1 -0
- package/dist/extensions/mcp/panel-keys.d.ts +21 -0
- package/dist/extensions/mcp/panel-keys.d.ts.map +1 -0
- package/dist/extensions/mcp/panel-keys.js +29 -0
- package/dist/extensions/mcp/panel-keys.js.map +1 -0
- package/dist/extensions/mcp/prompts.d.ts +63 -0
- package/dist/extensions/mcp/prompts.d.ts.map +1 -0
- package/dist/extensions/mcp/prompts.js +297 -0
- package/dist/extensions/mcp/prompts.js.map +1 -0
- package/dist/extensions/mcp/proxy-modes.d.ts +15 -0
- package/dist/extensions/mcp/proxy-modes.d.ts.map +1 -0
- package/dist/extensions/mcp/proxy-modes.js +1421 -0
- package/dist/extensions/mcp/proxy-modes.js.map +1 -0
- package/dist/extensions/mcp/request-headers-command.d.ts +11 -0
- package/dist/extensions/mcp/request-headers-command.d.ts.map +1 -0
- package/dist/extensions/mcp/request-headers-command.js +334 -0
- package/dist/extensions/mcp/request-headers-command.js.map +1 -0
- package/dist/extensions/mcp/resource-tools.d.ts +2 -0
- package/dist/extensions/mcp/resource-tools.d.ts.map +1 -0
- package/dist/extensions/mcp/resource-tools.js +15 -0
- package/dist/extensions/mcp/resource-tools.js.map +1 -0
- package/dist/extensions/mcp/runtime-owner.d.ts +14 -0
- package/dist/extensions/mcp/runtime-owner.d.ts.map +1 -0
- package/dist/extensions/mcp/runtime-owner.js +84 -0
- package/dist/extensions/mcp/runtime-owner.js.map +1 -0
- package/dist/extensions/mcp/sampling-handler.d.ts +18 -0
- package/dist/extensions/mcp/sampling-handler.d.ts.map +1 -0
- package/dist/extensions/mcp/sampling-handler.js +229 -0
- package/dist/extensions/mcp/sampling-handler.js.map +1 -0
- package/dist/extensions/mcp/search-ranking.d.ts +25 -0
- package/dist/extensions/mcp/search-ranking.d.ts.map +1 -0
- package/dist/extensions/mcp/search-ranking.js +234 -0
- package/dist/extensions/mcp/search-ranking.js.map +1 -0
- package/dist/extensions/mcp/server-manager.d.ts +116 -0
- package/dist/extensions/mcp/server-manager.d.ts.map +1 -0
- package/dist/extensions/mcp/server-manager.js +1052 -0
- package/dist/extensions/mcp/server-manager.js.map +1 -0
- package/dist/extensions/mcp/session-recovery.d.ts +27 -0
- package/dist/extensions/mcp/session-recovery.d.ts.map +1 -0
- package/dist/extensions/mcp/session-recovery.js +140 -0
- package/dist/extensions/mcp/session-recovery.js.map +1 -0
- package/dist/extensions/mcp/skills/mcp-scripting/SKILL.md +38 -0
- package/dist/extensions/mcp/state.d.ts +62 -0
- package/dist/extensions/mcp/state.d.ts.map +1 -0
- package/dist/extensions/mcp/state.js +2 -0
- package/dist/extensions/mcp/state.js.map +1 -0
- package/dist/extensions/mcp/tool-approval.d.ts +11 -0
- package/dist/extensions/mcp/tool-approval.d.ts.map +1 -0
- package/dist/extensions/mcp/tool-approval.js +156 -0
- package/dist/extensions/mcp/tool-approval.js.map +1 -0
- package/dist/extensions/mcp/tool-metadata.d.ts +11 -0
- package/dist/extensions/mcp/tool-metadata.d.ts.map +1 -0
- package/dist/extensions/mcp/tool-metadata.js +264 -0
- package/dist/extensions/mcp/tool-metadata.js.map +1 -0
- package/dist/extensions/mcp/tool-registrar.d.ts +21 -0
- package/dist/extensions/mcp/tool-registrar.d.ts.map +1 -0
- package/dist/extensions/mcp/tool-registrar.js +229 -0
- package/dist/extensions/mcp/tool-registrar.js.map +1 -0
- package/dist/extensions/mcp/tool-result-renderer.d.ts +83 -0
- package/dist/extensions/mcp/tool-result-renderer.d.ts.map +1 -0
- package/dist/extensions/mcp/tool-result-renderer.js +345 -0
- package/dist/extensions/mcp/tool-result-renderer.js.map +1 -0
- package/dist/extensions/mcp/ts-shape.d.ts +3 -0
- package/dist/extensions/mcp/ts-shape.d.ts.map +1 -0
- package/dist/extensions/mcp/ts-shape.js +160 -0
- package/dist/extensions/mcp/ts-shape.js.map +1 -0
- package/dist/extensions/mcp/types.d.ts +521 -0
- package/dist/extensions/mcp/types.d.ts.map +1 -0
- package/dist/extensions/mcp/types.js +294 -0
- package/dist/extensions/mcp/types.js.map +1 -0
- package/dist/extensions/mcp/ui-app-bridge-helpers.d.ts +7 -0
- package/dist/extensions/mcp/ui-app-bridge-helpers.d.ts.map +1 -0
- package/dist/extensions/mcp/ui-app-bridge-helpers.js +37 -0
- package/dist/extensions/mcp/ui-app-bridge-helpers.js.map +1 -0
- package/dist/extensions/mcp/ui-resource-handler.d.ts +18 -0
- package/dist/extensions/mcp/ui-resource-handler.d.ts.map +1 -0
- package/dist/extensions/mcp/ui-resource-handler.js +210 -0
- package/dist/extensions/mcp/ui-resource-handler.js.map +1 -0
- package/dist/extensions/mcp/ui-server.d.ts +52 -0
- package/dist/extensions/mcp/ui-server.d.ts.map +1 -0
- package/dist/extensions/mcp/ui-server.js +740 -0
- package/dist/extensions/mcp/ui-server.js.map +1 -0
- package/dist/extensions/mcp/ui-session.d.ts +40 -0
- package/dist/extensions/mcp/ui-session.d.ts.map +1 -0
- package/dist/extensions/mcp/ui-session.js +477 -0
- package/dist/extensions/mcp/ui-session.js.map +1 -0
- package/dist/extensions/mcp/ui-stream-types.d.ts +105 -0
- package/dist/extensions/mcp/ui-stream-types.d.ts.map +1 -0
- package/dist/extensions/mcp/ui-stream-types.js +61 -0
- package/dist/extensions/mcp/ui-stream-types.js.map +1 -0
- package/dist/extensions/mcp/ui-tool-visibility.d.ts +5 -0
- package/dist/extensions/mcp/ui-tool-visibility.d.ts.map +1 -0
- package/dist/extensions/mcp/ui-tool-visibility.js +27 -0
- package/dist/extensions/mcp/ui-tool-visibility.js.map +1 -0
- package/dist/extensions/mcp/unix-socket-transport.d.ts +15 -0
- package/dist/extensions/mcp/unix-socket-transport.d.ts.map +1 -0
- package/dist/extensions/mcp/unix-socket-transport.js +85 -0
- package/dist/extensions/mcp/unix-socket-transport.js.map +1 -0
- package/dist/extensions/mcp/utils.d.ts +32 -0
- package/dist/extensions/mcp/utils.d.ts.map +1 -0
- package/dist/extensions/mcp/utils.js +336 -0
- package/dist/extensions/mcp/utils.js.map +1 -0
- package/dist/extensions/subagent/index.d.ts +62 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +195 -9
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent-render.d.ts +3 -0
- package/dist/extensions/subagent-render.d.ts.map +1 -0
- package/dist/extensions/subagent-render.js +39 -0
- package/dist/extensions/subagent-render.js.map +1 -0
- package/dist/extensions/web-access.d.ts.map +1 -1
- package/dist/extensions/web-access.js +1 -1
- package/dist/extensions/web-access.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20992 -2003
- package/dist/index.js.map +1 -1
- package/dist/mcp-keyring-helper.cjs +89 -0
- package/dist/mcp-script-worker.mjs +130 -0
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/response-separator.d.ts +1 -1
- package/dist/modes/interactive/components/response-separator.d.ts.map +1 -1
- package/dist/modes/interactive/components/response-separator.js +1 -1
- package/dist/modes/interactive/components/response-separator.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +3 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +14 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/rpc-entry.js +20866 -1882
- package/dist/skills/mcp-scripting/SKILL.md +38 -0
- package/dist/utils/hidden-spawn.d.ts +29 -0
- package/dist/utils/hidden-spawn.d.ts.map +1 -0
- package/dist/utils/hidden-spawn.js +101 -0
- package/dist/utils/hidden-spawn.js.map +1 -0
- package/dist/vendor/ai.js +110 -35
- package/docs/mcp.md +178 -0
- package/docs/pi-subagents.md +75 -0
- package/docs/quickstart.md +1 -1
- package/docs/usage.md +2 -1
- package/examples/extensions/pi-subagents-worker-policy.ts +86 -0
- package/native/win32/build.mjs +95 -0
- package/native/win32/prebuilds/win32-x64/hidden-spawn.exe +0 -0
- package/native/win32/src/hidden-spawn.c +157 -0
- package/npm-shrinkwrap.json +728 -6
- package/package.json +21 -6
- package/dist/extensions/mcp/client.d.ts +0 -86
- package/dist/extensions/mcp/client.d.ts.map +0 -1
- package/dist/extensions/mcp/client.js +0 -275
- package/dist/extensions/mcp/client.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websearch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAsBtF,QAAA,MAAM,eAAe;;;EAKnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACpC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAcrD;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoB1D;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA4F1E;AAED,eAAO,MAAM,mBAAmB,0IAC2B,CAAC","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"
|
|
1
|
+
{"version":3,"file":"websearch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAsBtF,QAAA,MAAM,eAAe;;;EAKnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACpC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAcrD;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoB1D;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA4F1E;AAED,eAAO,MAAM,mBAAmB,0IAC2B,CAAC","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use web_search for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tlet response: Response;\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\t\tif (response.status === 429) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\tapiKey\n\t\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tlet body: string;\n\t\t\t\ttry {\n\t\t\t\t\tbody = await response.text();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search response failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\tconst text = extractResultText(parseMcpPayload(body));\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
|
|
@@ -68,11 +68,11 @@ export function extractResultText(payload) {
|
|
|
68
68
|
}
|
|
69
69
|
export function createWebSearchToolDefinition(options) {
|
|
70
70
|
return {
|
|
71
|
-
name: "
|
|
72
|
-
label: "
|
|
71
|
+
name: "web_search",
|
|
72
|
+
label: "web_search",
|
|
73
73
|
description: "Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.",
|
|
74
74
|
promptSnippet: "Search the web for current information",
|
|
75
|
-
promptGuidelines: ["Use
|
|
75
|
+
promptGuidelines: ["Use web_search for anything outside the repository; do not guess at current facts."],
|
|
76
76
|
parameters: websearchSchema,
|
|
77
77
|
async execute(_toolCallId, { query, numResults }, signal) {
|
|
78
78
|
const doFetch = options?.fetchImpl ?? fetch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websearch.js","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"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","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use websearch for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tlet response: Response;\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\t\tif (response.status === 429) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\tapiKey\n\t\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tlet body: string;\n\t\t\t\ttry {\n\t\t\t\t\tbody = await response.text();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search response failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\tconst text = extractResultText(parseMcpPayload(body));\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
|
|
1
|
+
{"version":3,"file":"websearch.js","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"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","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"web_search\",\n\t\tlabel: \"web_search\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use web_search for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tlet response: Response;\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\t\tif (response.status === 429) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\tapiKey\n\t\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tlet body: string;\n\t\t\t\ttry {\n\t\t\t\t\tbody = await response.text();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search response failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\tconst text = extractResultText(parseMcpPayload(body));\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
* GitNexus hub connection settings.
|
|
3
3
|
*
|
|
4
4
|
* Resolution order (first hit wins):
|
|
5
|
-
* 1. Environment: GITNEXUS_HUB_URL
|
|
5
|
+
* 1. Environment: GITNEXUS_HUB_URL, then HUB_URL (process env or a `.env` file)
|
|
6
6
|
* 2. Config file: ~/.config/nexus/auth.json ({ hubUrl, token })
|
|
7
|
+
* 3. Production default
|
|
8
|
+
*
|
|
9
|
+
* HUB_URL is the hub origin (`https://host`). The MCP client needs the
|
|
10
|
+
* `.../mcp` endpoint, so a bare origin gets `/mcp` appended. GITNEXUS_HUB_URL
|
|
11
|
+
* may already be that endpoint. Token: GITNEXUS_TOKEN, then the config file.
|
|
7
12
|
*
|
|
8
13
|
* The hub endpoint is the Streamable-HTTP MCP endpoint (".../mcp"). Bearer
|
|
9
14
|
* tokens are hub API tokens; an empty token allowlist on the hub means the
|
|
@@ -17,6 +22,15 @@ export interface GitNexusConfig {
|
|
|
17
22
|
deviceName?: string | undefined;
|
|
18
23
|
}
|
|
19
24
|
export declare const DEFAULT_HUB_URL = "https://gitnexus-enterprise-production.up.railway.app/mcp";
|
|
25
|
+
/** MCP endpoint for a hub origin or an already-qualified `.../mcp` URL. */
|
|
26
|
+
export declare function toMcpHubUrl(value: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Hub MCP endpoint.
|
|
29
|
+
*
|
|
30
|
+
* GITNEXUS_HUB_URL, then HUB_URL from the environment or `.env`, then a saved
|
|
31
|
+
* auth.json value, then the production default.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveHubUrl(savedHubUrl?: string): string;
|
|
20
34
|
/** Resolve hub settings. Returns undefined when no token is configured anywhere. */
|
|
21
35
|
export declare function resolveGitNexusConfig(): GitNexusConfig | undefined;
|
|
22
36
|
/** Persist hub settings from `/login`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/config.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,eAAO,MAAM,eAAe,8DAA8D,CAAC;AAE3F,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;AA0DD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1D;AAoBD,oFAAoF;AACpF,wBAAgB,qBAAqB,IAAI,cAAc,GAAG,SAAS,CAQlE;AAED,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAIjE;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * GitNexus hub connection settings.\n *\n * Resolution order (first hit wins):\n * 1. Environment: GITNEXUS_HUB_URL, then HUB_URL (process env or a `.env` file)\n * 2. Config file: ~/.config/nexus/auth.json ({ hubUrl, token })\n * 3. Production default\n *\n * HUB_URL is the hub origin (`https://host`). The MCP client needs the\n * `.../mcp` endpoint, so a bare origin gets `/mcp` appended. GITNEXUS_HUB_URL\n * may already be that endpoint. Token: GITNEXUS_TOKEN, then the config file.\n *\n * The hub endpoint is the Streamable-HTTP MCP endpoint (\".../mcp\"). Bearer\n * tokens are hub API tokens; an empty token allowlist on the hub means the\n * token can see every repo of its user identity.\n */\n\nexport interface GitNexusConfig {\n\thubUrl: string;\n\ttoken: string;\n\t/** Hub tokens can be device-bound; these headers must then match the binding. */\n\tdeviceFingerprint?: string | undefined;\n\tdeviceName?: string | undefined;\n}\n\nexport const DEFAULT_HUB_URL = \"https://gitnexus-enterprise-production.up.railway.app/mcp\";\n\n/** MCP endpoint for a hub origin or an already-qualified `.../mcp` URL. */\nexport function toMcpHubUrl(value: string): string {\n\tconst trimmed = value.trim().replace(/\\/+$/, \"\");\n\tif (trimmed.endsWith(\"/mcp\")) return trimmed;\n\treturn `${trimmed}/mcp`;\n}\n\nfunction readHubUrlKey(path: string): string | undefined {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, \"utf8\");\n\t} catch {\n\t\treturn undefined;\n\t}\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\t\tconst eq = trimmed.indexOf(\"=\");\n\t\tif (eq <= 0) continue;\n\t\tif (trimmed.slice(0, eq).trim() !== \"HUB_URL\") continue;\n\t\tlet value = trimmed.slice(eq + 1).trim();\n\t\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\t\tvalue = value.slice(1, -1);\n\t\t}\n\t\treturn value || undefined;\n\t}\n\treturn undefined;\n}\n\n/**\n * HUB_URL from a `.env` file next to this checkout.\n *\n * Walks up from the working directory and from this module so `npm start`\n * from the repo root and a launch from a nested package both find\n * `nexus-cli/.env`. Skipped under Vitest so a developer `.env` does not\n * change the default the tests assert.\n */\nfunction readDotEnvHubUrl(): string | undefined {\n\tif (process.env.VITEST) return undefined;\n\tconst starts = [process.cwd()];\n\ttry {\n\t\tstarts.push(dirname(fileURLToPath(import.meta.url)));\n\t} catch {\n\t\t// import.meta.url is always present in the ESM build; ignore if a test bundler drops it.\n\t}\n\tconst seen = new Set<string>();\n\tfor (const start of starts) {\n\t\tlet dir = start;\n\t\tfor (let depth = 0; depth < 8; depth++) {\n\t\t\tconst candidate = join(dir, \".env\");\n\t\t\tif (!seen.has(candidate)) {\n\t\t\t\tseen.add(candidate);\n\t\t\t\tconst value = readHubUrlKey(candidate);\n\t\t\t\tif (value) return value;\n\t\t\t}\n\t\t\tconst parent = dirname(dir);\n\t\t\tif (parent === dir) break;\n\t\t\tdir = parent;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Hub MCP endpoint.\n *\n * GITNEXUS_HUB_URL, then HUB_URL from the environment or `.env`, then a saved\n * auth.json value, then the production default.\n */\nexport function resolveHubUrl(savedHubUrl?: string): string {\n\tconst fromEnv = process.env.GITNEXUS_HUB_URL?.trim() || process.env.HUB_URL?.trim() || readDotEnvHubUrl();\n\tif (fromEnv) return toMcpHubUrl(fromEnv);\n\tif (savedHubUrl) return savedHubUrl;\n\treturn DEFAULT_HUB_URL;\n}\n\nfunction configDir(): string {\n\treturn join(homedir(), \".config\", \"nexus\");\n}\n\nfunction configPath(): string {\n\treturn join(configDir(), \"auth.json\");\n}\n\nfunction readConfigFile(): Partial<GitNexusConfig> {\n\ttry {\n\t\tconst raw = readFileSync(configPath(), \"utf8\");\n\t\tconst parsed = JSON.parse(raw) as Partial<GitNexusConfig>;\n\t\treturn typeof parsed === \"object\" && parsed !== null ? parsed : {};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n/** Resolve hub settings. Returns undefined when no token is configured anywhere. */\nexport function resolveGitNexusConfig(): GitNexusConfig | undefined {\n\tconst file = readConfigFile();\n\tconst token = process.env.GITNEXUS_TOKEN ?? file.token;\n\tif (!token) return undefined;\n\tconst hubUrl = resolveHubUrl(file.hubUrl);\n\tconst deviceFingerprint = process.env.GITNEXUS_DEVICE_FP ?? file.deviceFingerprint;\n\tconst deviceName = process.env.GITNEXUS_DEVICE_NAME ?? file.deviceName;\n\treturn { hubUrl, token, deviceFingerprint, deviceName };\n}\n\n/** Persist hub settings from `/login`. */\nexport function saveGitNexusConfig(config: GitNexusConfig): string {\n\tif (!existsSync(configDir())) mkdirSync(configDir(), { recursive: true, mode: 0o700 });\n\twriteFileSync(configPath(), `${JSON.stringify(config, null, \"\\t\")}\\n`, { mode: 0o600 });\n\treturn configPath();\n}\n\nexport function gitNexusConfigPath(): string {\n\treturn configPath();\n}\n"]}
|
|
@@ -1,7 +1,91 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
export const DEFAULT_HUB_URL = "https://gitnexus-enterprise-production.up.railway.app/mcp";
|
|
6
|
+
/** MCP endpoint for a hub origin or an already-qualified `.../mcp` URL. */
|
|
7
|
+
export function toMcpHubUrl(value) {
|
|
8
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
9
|
+
if (trimmed.endsWith("/mcp"))
|
|
10
|
+
return trimmed;
|
|
11
|
+
return `${trimmed}/mcp`;
|
|
12
|
+
}
|
|
13
|
+
function readHubUrlKey(path) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = readFileSync(path, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
for (const line of raw.split("\n")) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
24
|
+
continue;
|
|
25
|
+
const eq = trimmed.indexOf("=");
|
|
26
|
+
if (eq <= 0)
|
|
27
|
+
continue;
|
|
28
|
+
if (trimmed.slice(0, eq).trim() !== "HUB_URL")
|
|
29
|
+
continue;
|
|
30
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
31
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
32
|
+
value = value.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
return value || undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* HUB_URL from a `.env` file next to this checkout.
|
|
40
|
+
*
|
|
41
|
+
* Walks up from the working directory and from this module so `npm start`
|
|
42
|
+
* from the repo root and a launch from a nested package both find
|
|
43
|
+
* `nexus-cli/.env`. Skipped under Vitest so a developer `.env` does not
|
|
44
|
+
* change the default the tests assert.
|
|
45
|
+
*/
|
|
46
|
+
function readDotEnvHubUrl() {
|
|
47
|
+
if (process.env.VITEST)
|
|
48
|
+
return undefined;
|
|
49
|
+
const starts = [process.cwd()];
|
|
50
|
+
try {
|
|
51
|
+
starts.push(dirname(fileURLToPath(import.meta.url)));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// import.meta.url is always present in the ESM build; ignore if a test bundler drops it.
|
|
55
|
+
}
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const start of starts) {
|
|
58
|
+
let dir = start;
|
|
59
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
60
|
+
const candidate = join(dir, ".env");
|
|
61
|
+
if (!seen.has(candidate)) {
|
|
62
|
+
seen.add(candidate);
|
|
63
|
+
const value = readHubUrlKey(candidate);
|
|
64
|
+
if (value)
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
const parent = dirname(dir);
|
|
68
|
+
if (parent === dir)
|
|
69
|
+
break;
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hub MCP endpoint.
|
|
77
|
+
*
|
|
78
|
+
* GITNEXUS_HUB_URL, then HUB_URL from the environment or `.env`, then a saved
|
|
79
|
+
* auth.json value, then the production default.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveHubUrl(savedHubUrl) {
|
|
82
|
+
const fromEnv = process.env.GITNEXUS_HUB_URL?.trim() || process.env.HUB_URL?.trim() || readDotEnvHubUrl();
|
|
83
|
+
if (fromEnv)
|
|
84
|
+
return toMcpHubUrl(fromEnv);
|
|
85
|
+
if (savedHubUrl)
|
|
86
|
+
return savedHubUrl;
|
|
87
|
+
return DEFAULT_HUB_URL;
|
|
88
|
+
}
|
|
5
89
|
function configDir() {
|
|
6
90
|
return join(homedir(), ".config", "nexus");
|
|
7
91
|
}
|
|
@@ -24,7 +108,7 @@ export function resolveGitNexusConfig() {
|
|
|
24
108
|
const token = process.env.GITNEXUS_TOKEN ?? file.token;
|
|
25
109
|
if (!token)
|
|
26
110
|
return undefined;
|
|
27
|
-
const hubUrl =
|
|
111
|
+
const hubUrl = resolveHubUrl(file.hubUrl);
|
|
28
112
|
const deviceFingerprint = process.env.GITNEXUS_DEVICE_FP ?? file.deviceFingerprint;
|
|
29
113
|
const deviceName = process.env.GITNEXUS_DEVICE_NAME ?? file.deviceName;
|
|
30
114
|
return { hubUrl, token, deviceFingerprint, deviceName };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/config.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * GitNexus hub connection settings.\n *\n * Resolution order (first hit wins):\n * 1. Environment: GITNEXUS_HUB_URL, then HUB_URL (process env or a `.env` file)\n * 2. Config file: ~/.config/nexus/auth.json ({ hubUrl, token })\n * 3. Production default\n *\n * HUB_URL is the hub origin (`https://host`). The MCP client needs the\n * `.../mcp` endpoint, so a bare origin gets `/mcp` appended. GITNEXUS_HUB_URL\n * may already be that endpoint. Token: GITNEXUS_TOKEN, then the config file.\n *\n * The hub endpoint is the Streamable-HTTP MCP endpoint (\".../mcp\"). Bearer\n * tokens are hub API tokens; an empty token allowlist on the hub means the\n * token can see every repo of its user identity.\n */\n\nexport interface GitNexusConfig {\n\thubUrl: string;\n\ttoken: string;\n\t/** Hub tokens can be device-bound; these headers must then match the binding. */\n\tdeviceFingerprint?: string | undefined;\n\tdeviceName?: string | undefined;\n}\n\nexport const DEFAULT_HUB_URL = \"https://gitnexus-enterprise-production.up.railway.app/mcp\";\n\n/** MCP endpoint for a hub origin or an already-qualified `.../mcp` URL. */\nexport function toMcpHubUrl(value: string): string {\n\tconst trimmed = value.trim().replace(/\\/+$/, \"\");\n\tif (trimmed.endsWith(\"/mcp\")) return trimmed;\n\treturn `${trimmed}/mcp`;\n}\n\nfunction readHubUrlKey(path: string): string | undefined {\n\tlet raw: string;\n\ttry {\n\t\traw = readFileSync(path, \"utf8\");\n\t} catch {\n\t\treturn undefined;\n\t}\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\t\tconst eq = trimmed.indexOf(\"=\");\n\t\tif (eq <= 0) continue;\n\t\tif (trimmed.slice(0, eq).trim() !== \"HUB_URL\") continue;\n\t\tlet value = trimmed.slice(eq + 1).trim();\n\t\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\t\tvalue = value.slice(1, -1);\n\t\t}\n\t\treturn value || undefined;\n\t}\n\treturn undefined;\n}\n\n/**\n * HUB_URL from a `.env` file next to this checkout.\n *\n * Walks up from the working directory and from this module so `npm start`\n * from the repo root and a launch from a nested package both find\n * `nexus-cli/.env`. Skipped under Vitest so a developer `.env` does not\n * change the default the tests assert.\n */\nfunction readDotEnvHubUrl(): string | undefined {\n\tif (process.env.VITEST) return undefined;\n\tconst starts = [process.cwd()];\n\ttry {\n\t\tstarts.push(dirname(fileURLToPath(import.meta.url)));\n\t} catch {\n\t\t// import.meta.url is always present in the ESM build; ignore if a test bundler drops it.\n\t}\n\tconst seen = new Set<string>();\n\tfor (const start of starts) {\n\t\tlet dir = start;\n\t\tfor (let depth = 0; depth < 8; depth++) {\n\t\t\tconst candidate = join(dir, \".env\");\n\t\t\tif (!seen.has(candidate)) {\n\t\t\t\tseen.add(candidate);\n\t\t\t\tconst value = readHubUrlKey(candidate);\n\t\t\t\tif (value) return value;\n\t\t\t}\n\t\t\tconst parent = dirname(dir);\n\t\t\tif (parent === dir) break;\n\t\t\tdir = parent;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Hub MCP endpoint.\n *\n * GITNEXUS_HUB_URL, then HUB_URL from the environment or `.env`, then a saved\n * auth.json value, then the production default.\n */\nexport function resolveHubUrl(savedHubUrl?: string): string {\n\tconst fromEnv = process.env.GITNEXUS_HUB_URL?.trim() || process.env.HUB_URL?.trim() || readDotEnvHubUrl();\n\tif (fromEnv) return toMcpHubUrl(fromEnv);\n\tif (savedHubUrl) return savedHubUrl;\n\treturn DEFAULT_HUB_URL;\n}\n\nfunction configDir(): string {\n\treturn join(homedir(), \".config\", \"nexus\");\n}\n\nfunction configPath(): string {\n\treturn join(configDir(), \"auth.json\");\n}\n\nfunction readConfigFile(): Partial<GitNexusConfig> {\n\ttry {\n\t\tconst raw = readFileSync(configPath(), \"utf8\");\n\t\tconst parsed = JSON.parse(raw) as Partial<GitNexusConfig>;\n\t\treturn typeof parsed === \"object\" && parsed !== null ? parsed : {};\n\t} catch {\n\t\treturn {};\n\t}\n}\n\n/** Resolve hub settings. Returns undefined when no token is configured anywhere. */\nexport function resolveGitNexusConfig(): GitNexusConfig | undefined {\n\tconst file = readConfigFile();\n\tconst token = process.env.GITNEXUS_TOKEN ?? file.token;\n\tif (!token) return undefined;\n\tconst hubUrl = resolveHubUrl(file.hubUrl);\n\tconst deviceFingerprint = process.env.GITNEXUS_DEVICE_FP ?? file.deviceFingerprint;\n\tconst deviceName = process.env.GITNEXUS_DEVICE_NAME ?? file.deviceName;\n\treturn { hubUrl, token, deviceFingerprint, deviceName };\n}\n\n/** Persist hub settings from `/login`. */\nexport function saveGitNexusConfig(config: GitNexusConfig): string {\n\tif (!existsSync(configDir())) mkdirSync(configDir(), { recursive: true, mode: 0o700 });\n\twriteFileSync(configPath(), `${JSON.stringify(config, null, \"\\t\")}\\n`, { mode: 0o600 });\n\treturn configPath();\n}\n\nexport function gitNexusConfigPath(): string {\n\treturn configPath();\n}\n"]}
|
|
@@ -2,12 +2,12 @@ import type { ExtensionAPI } from "../../core/extensions/types.ts";
|
|
|
2
2
|
import type { DelegationCapability } from "../subagent/index.ts";
|
|
3
3
|
import { type GitNexusConfig } from "./config.ts";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Nexus Agent mode: models served through the hub's LLM gateway.
|
|
6
6
|
*
|
|
7
7
|
* The hub decides which model "auto" is and never tells the CLI — the policy
|
|
8
8
|
* endpoint serves budget numbers (context window, output cap), not a name, so
|
|
9
9
|
* there is nothing here that could display one. The entry the user sees is
|
|
10
|
-
* "
|
|
10
|
+
* "Nexus Agent", and the hub can move the underlying model without a CLI release.
|
|
11
11
|
*
|
|
12
12
|
* Providers must exist before the initial model is resolved, which happens
|
|
13
13
|
* before any network round-trip could finish, so registration reads a policy
|
|
@@ -18,7 +18,7 @@ import { type GitNexusConfig } from "./config.ts";
|
|
|
18
18
|
*
|
|
19
19
|
* The gate runs at every startup, so the cache is at most one session stale.
|
|
20
20
|
* Entitlement is the hub's call: a 404 from the policy endpoint (feature off
|
|
21
|
-
* for this account) deletes the cache and the next session has no
|
|
21
|
+
* for this account) deletes the cache and the next session has no Nexus Agent entry.
|
|
22
22
|
*/
|
|
23
23
|
export interface GatewayPolicy {
|
|
24
24
|
configured: boolean;
|
|
@@ -41,7 +41,7 @@ export declare function readGatewayCache(path?: string): GatewayPolicy | undefin
|
|
|
41
41
|
* Never throws: auto mode is additive, and a hub outage at startup must not
|
|
42
42
|
* cost the user their session. A definitive "no" (404: not entitled; 401/403:
|
|
43
43
|
* bad credential) clears the cache; a transient failure keeps it, so a flaky
|
|
44
|
-
* network does not flap the
|
|
44
|
+
* network does not flap the Nexus Agent entry in and out of the model list.
|
|
45
45
|
*/
|
|
46
46
|
export declare function refreshGatewayPolicy(config: GitNexusConfig | undefined, options?: {
|
|
47
47
|
fetchImpl?: typeof fetch;
|
|
@@ -49,11 +49,11 @@ export declare function refreshGatewayPolicy(config: GitNexusConfig | undefined,
|
|
|
49
49
|
timeoutMs?: number;
|
|
50
50
|
}): Promise<GatewayPolicy | undefined>;
|
|
51
51
|
/**
|
|
52
|
-
* Register the
|
|
52
|
+
* Register the Nexus Agent entry. Synchronous — called from the extension factory so
|
|
53
53
|
* the provider exists before initial model resolution.
|
|
54
54
|
*
|
|
55
55
|
* No config, no cache, or a gateway with no upstream key all mean no
|
|
56
|
-
* registration:
|
|
56
|
+
* registration: a Nexus Agent entry that cannot serve a request is worse than none.
|
|
57
57
|
*/
|
|
58
58
|
export declare function registerAutoProvider(pi: ExtensionAPI, config: GitNexusConfig | undefined, policy: GatewayPolicy | undefined): boolean;
|
|
59
59
|
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,UAAU,CAAC,EAAE,oBAAoB,CAAC;CAClC;AAwCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIzF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,MAA2B,GAAG,aAAa,GAAG,SAAS,CAc7F;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAGzC,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,GAAE;IACR,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACd,GACJ,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAwCpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,MAAM,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAwCT","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport type { DelegationCapability } from \"../subagent/index.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,UAAU,CAAC,EAAE,oBAAoB,CAAC;CAClC;AAwCD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIzF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,MAA2B,GAAG,aAAa,GAAG,SAAS,CAc7F;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAGzC,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,GAAE;IACR,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACd,GACJ,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAwCpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,MAAM,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAwCT","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport type { DelegationCapability } from \"../subagent/index.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Nexus Agent mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Nexus Agent\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Nexus Agent entry.\n */\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n\tdelegation?: DelegationCapability;\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nfunction parseDelegation(value: unknown): DelegationCapability | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst delegation = value as Record<string, unknown>;\n\tif (\n\t\tdelegation.enabled !== true ||\n\t\tdelegation.version !== 1 ||\n\t\ttypeof delegation.maxParallel !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxParallel) ||\n\t\tdelegation.maxParallel < 1 ||\n\t\ttypeof delegation.maxTaskBytes !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxTaskBytes) ||\n\t\tdelegation.maxTaskBytes < 1 ||\n\t\ttypeof delegation.workerTimeoutMs !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.workerTimeoutMs) ||\n\t\tdelegation.workerTimeoutMs < 1\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tenabled: true,\n\t\tversion: 1,\n\t\tmaxParallel: delegation.maxParallel,\n\t\tmaxTaskBytes: delegation.maxTaskBytes,\n\t\tworkerTimeoutMs: delegation.workerTimeoutMs,\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t\tdelegation: parseDelegation(parsed.delegation),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Nexus Agent entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t\tdelegation: parseDelegation(body.delegation),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Nexus Agent entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: a Nexus Agent entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Nexus Agent\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
|
|
@@ -73,7 +73,7 @@ export function readGatewayCache(path = gatewayCachePath()) {
|
|
|
73
73
|
* Never throws: auto mode is additive, and a hub outage at startup must not
|
|
74
74
|
* cost the user their session. A definitive "no" (404: not entitled; 401/403:
|
|
75
75
|
* bad credential) clears the cache; a transient failure keeps it, so a flaky
|
|
76
|
-
* network does not flap the
|
|
76
|
+
* network does not flap the Nexus Agent entry in and out of the model list.
|
|
77
77
|
*/
|
|
78
78
|
export async function refreshGatewayPolicy(
|
|
79
79
|
// No default: an explicit `undefined` must mean signed out, never "read the
|
|
@@ -122,11 +122,11 @@ config, options = {}) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
|
-
* Register the
|
|
125
|
+
* Register the Nexus Agent entry. Synchronous — called from the extension factory so
|
|
126
126
|
* the provider exists before initial model resolution.
|
|
127
127
|
*
|
|
128
128
|
* No config, no cache, or a gateway with no upstream key all mean no
|
|
129
|
-
* registration:
|
|
129
|
+
* registration: a Nexus Agent entry that cannot serve a request is worse than none.
|
|
130
130
|
*/
|
|
131
131
|
export function registerAutoProvider(pi, config, policy) {
|
|
132
132
|
if (!config || !policy || !policy.configured)
|
|
@@ -145,7 +145,7 @@ export function registerAutoProvider(pi, config, policy) {
|
|
|
145
145
|
models: [
|
|
146
146
|
{
|
|
147
147
|
id: AUTO_MODEL_ID,
|
|
148
|
-
name: "
|
|
148
|
+
name: "Nexus Agent",
|
|
149
149
|
// The agent already owns a stable session ID. Forward it only to the
|
|
150
150
|
// Hub, which hashes it before applying provider-specific cache affinity.
|
|
151
151
|
compat: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EAAuB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA8B9C,SAAS,UAAU,CAAC,KAAc,EAA0B;IAC3D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,KAAK,GAAG,KAAgC,CAAC;IAC/C,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACxF,OAAO;QACN,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,CAAC,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9E,CAAC;AAAA,CACF;AAED,SAAS,eAAe,CAAC,KAAc,EAAoC;IAC1E,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,UAAU,GAAG,KAAgC,CAAC;IACpD,IACC,UAAU,CAAC,OAAO,KAAK,IAAI;QAC3B,UAAU,CAAC,OAAO,KAAK,CAAC;QACxB,OAAO,UAAU,CAAC,WAAW,KAAK,QAAQ;QAC1C,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,WAAW,CAAC;QAC7C,UAAU,CAAC,WAAW,GAAG,CAAC;QAC1B,OAAO,UAAU,CAAC,YAAY,KAAK,QAAQ;QAC3C,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,YAAY,CAAC;QAC9C,UAAU,CAAC,YAAY,GAAG,CAAC;QAC3B,OAAO,UAAU,CAAC,eAAe,KAAK,QAAQ;QAC9C,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,eAAe,CAAC;QACjD,UAAU,CAAC,eAAe,GAAG,CAAC,EAC7B,CAAC;QACF,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO;QACN,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,UAAU,CAAC,WAAW;QACnC,YAAY,EAAE,UAAU,CAAC,YAAY;QACrC,eAAe,EAAE,UAAU,CAAC,eAAe;KAC3C,CAAC;AAAA,CACF;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAiC,EAAsB;IAC1F,IAAI,CAAC,MAAM,EAAE,KAAK;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChG,OAAO,kBAAkB,SAAS,CAAC,cAAc,EAAE,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;AAAA,CAC/F;AAED,MAAM,UAAU,gBAAgB,GAAW;IAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;AAAA,CAC3D;AAED,oEAAoE;AACpE,MAAM,UAAU,gBAAgB,CAAC,IAAI,GAAW,gBAAgB,EAAE,EAA6B;IAC9F,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA2B,CAAC;QAChF,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACvG,OAAO;YACN,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,IAAI;YACtC,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC;YAC/B,UAAU,EAAE,eAAe,CAAC,MAAM,CAAC,UAAU,CAAC;SAC9C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;AACzC,4EAA4E;AAC5E,6EAA2E;AAC3E,MAAkC,EAClC,OAAO,GAIH,EAAE,EAC+B;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;IAC1D,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAE7C,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE;YAC3E,uEAAuE;YACvE,mEAAmE;YACnE,OAAO,EAAE;gBACR,aAAa,EAAE,UAAU,MAAM,CAAC,KAAK,EAAE;gBACvC,MAAM,EAAE,kBAAkB;gBAC1B,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpE;YACD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;SACxD,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACnF,MAAM,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAErD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;QAC/D,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClF,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,MAAM,GAAkB;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU,KAAK,IAAI;YACpC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;YAC7B,UAAU,EAAE,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC;SAC5C,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAAE,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACrG,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC;AAAA,CACD;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CACnC,EAAgB,EAChB,MAAkC,EAClC,MAAiC,EACvB;IACV,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAE3D,EAAE,CAAC,gBAAgB,CAAC,gBAAgB,EAAE;QACrC,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM;QAC3C,MAAM,EAAE,MAAM,CAAC,KAAK;QACpB,UAAU,EAAE,IAAI;QAChB,yEAAyE;QACzE,OAAO,EAAE;YACR,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpE;QACD,GAAG,EAAE,oBAAoB;QACzB,MAAM,EAAE;YACP;gBACC,EAAE,EAAE,aAAa;gBACjB,IAAI,EAAE,MAAM;gBACZ,qEAAqE;gBACrE,yEAAyE;gBACzE,MAAM,EAAE;oBACP,0BAA0B,EAAE,IAAI;oBAChC,qBAAqB,EAAE,kBAAkB;iBACzC;gBACD,qEAAqE;gBACrE,uEAAqE;gBACrE,uBAAuB;gBACvB,SAAS,EAAE,KAAK;gBAChB,oEAAoE;gBACpE,yDAAyD;gBACzD,KAAK,EAAE,CAAC,MAAM,CAAC;gBACf,+DAA+D;gBAC/D,qCAAqC;gBACrC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;gBAC1D,aAAa,EAAE,MAAM,CAAC,aAAa;gBACnC,SAAS,EAAE,MAAM,CAAC,SAAS;aAC3B;SACD;KACD,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AAAA,CACZ","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport type { DelegationCapability } from \"../subagent/index.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n\tdelegation?: DelegationCapability;\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nfunction parseDelegation(value: unknown): DelegationCapability | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst delegation = value as Record<string, unknown>;\n\tif (\n\t\tdelegation.enabled !== true ||\n\t\tdelegation.version !== 1 ||\n\t\ttypeof delegation.maxParallel !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxParallel) ||\n\t\tdelegation.maxParallel < 1 ||\n\t\ttypeof delegation.maxTaskBytes !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxTaskBytes) ||\n\t\tdelegation.maxTaskBytes < 1 ||\n\t\ttypeof delegation.workerTimeoutMs !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.workerTimeoutMs) ||\n\t\tdelegation.workerTimeoutMs < 1\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tenabled: true,\n\t\tversion: 1,\n\t\tmaxParallel: delegation.maxParallel,\n\t\tmaxTaskBytes: delegation.maxTaskBytes,\n\t\tworkerTimeoutMs: delegation.workerTimeoutMs,\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t\tdelegation: parseDelegation(parsed.delegation),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t\tdelegation: parseDelegation(body.delegation),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport type { DelegationCapability } from \"../subagent/index.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Nexus Agent mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Nexus Agent\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Nexus Agent entry.\n */\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n\tdelegation?: DelegationCapability;\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nfunction parseDelegation(value: unknown): DelegationCapability | undefined {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst delegation = value as Record<string, unknown>;\n\tif (\n\t\tdelegation.enabled !== true ||\n\t\tdelegation.version !== 1 ||\n\t\ttypeof delegation.maxParallel !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxParallel) ||\n\t\tdelegation.maxParallel < 1 ||\n\t\ttypeof delegation.maxTaskBytes !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.maxTaskBytes) ||\n\t\tdelegation.maxTaskBytes < 1 ||\n\t\ttypeof delegation.workerTimeoutMs !== \"number\" ||\n\t\t!Number.isSafeInteger(delegation.workerTimeoutMs) ||\n\t\tdelegation.workerTimeoutMs < 1\n\t) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tenabled: true,\n\t\tversion: 1,\n\t\tmaxParallel: delegation.maxParallel,\n\t\tmaxTaskBytes: delegation.maxTaskBytes,\n\t\tworkerTimeoutMs: delegation.workerTimeoutMs,\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t\tdelegation: parseDelegation(parsed.delegation),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Nexus Agent entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t\tdelegation: parseDelegation(body.delegation),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Nexus Agent entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: a Nexus Agent entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Nexus Agent\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAsBnE,2EAA2E;AAC3E,wBAAgB,wBAAwB,CACvC,SAAS,GAAE,MAAwB,EACnC,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAChD,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAkNhE","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport { isDelegatedWorkerProcess, registerDelegateTaskTool } from \"../subagent/index.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet delegationRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tlet gatewayPolicy = readGatewayCache();\n\tconst initialConfig = resolveGitNexusConfig();\n\tregisterAutoProvider(pi, initialConfig, gatewayPolicy);\n\n\tfunction registerDelegationIfAvailable(): void {\n\t\tif (\n\t\t\tdelegationRegistered ||\n\t\t\tisDelegatedWorkerProcess() ||\n\t\t\t!resolveGitNexusConfig() ||\n\t\t\t!gatewayPolicy?.delegation\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tregisterDelegateTaskTool(pi, gatewayPolicy.delegation);\n\t\tdelegationRegistered = true;\n\t}\n\n\tregisterDelegationIfAvailable();\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tregisterDelegationIfAvailable();\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AA2BnE,2EAA2E;AAC3E,wBAAgB,wBAAwB,CACvC,SAAS,GAAE,MAAwB,EACnC,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAChD,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAoNhE","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport {\n\tapplyNexusSubagentEnvAliases,\n\tisDelegatedWorkerProcess,\n\tisExternalDelegationMode,\n\tregisterDelegateTaskTool,\n} from \"../subagent/index.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { resolveGitNexusConfig, resolveHubUrl, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet delegationRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Nexus Agent must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tapplyNexusSubagentEnvAliases();\n\tlet gatewayPolicy = readGatewayCache();\n\tconst initialConfig = resolveGitNexusConfig();\n\tregisterAutoProvider(pi, initialConfig, gatewayPolicy);\n\n\tfunction registerDelegationIfAvailable(): void {\n\t\tif (\n\t\t\tdelegationRegistered ||\n\t\t\tisDelegatedWorkerProcess() ||\n\t\t\tisExternalDelegationMode() ||\n\t\t\t!resolveGitNexusConfig() ||\n\t\t\t!gatewayPolicy?.delegation\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tregisterDelegateTaskTool(pi, gatewayPolicy.delegation);\n\t\tdelegationRegistered = true;\n\t}\n\n\tregisterDelegationIfAvailable();\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tregisterDelegationIfAvailable();\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = resolveHubUrl();\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|