@f5-sales-demo/xcsh 19.51.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 +7073 -0
- package/README.md +14 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +109 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/todo/index.ts +206 -0
- package/examples/extensions/README.md +143 -0
- package/examples/extensions/api-demo.ts +89 -0
- package/examples/extensions/chalk-logger.ts +25 -0
- package/examples/extensions/hello.ts +32 -0
- package/examples/extensions/pirate.ts +43 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/reload-runtime.ts +37 -0
- package/examples/extensions/todo.ts +296 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +48 -0
- package/examples/hooks/confirm-destructive.ts +58 -0
- package/examples/hooks/custom-compaction.ts +115 -0
- package/examples/hooks/dirty-repo-guard.ts +51 -0
- package/examples/hooks/file-trigger.ts +40 -0
- package/examples/hooks/git-checkpoint.ts +52 -0
- package/examples/hooks/handoff.ts +149 -0
- package/examples/hooks/permission-gate.ts +33 -0
- package/examples/hooks/protected-paths.ts +29 -0
- package/examples/hooks/qna.ts +118 -0
- package/examples/hooks/status-line.ts +39 -0
- package/examples/sdk/01-minimal.ts +21 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +43 -0
- package/examples/sdk/04-skills.ts +43 -0
- package/examples/sdk/06-extensions.ts +80 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +35 -0
- package/examples/sdk/08-prompt-templates.ts +41 -0
- package/examples/sdk/08-slash-commands.ts +46 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
- package/examples/sdk/11-sessions.ts +47 -0
- package/examples/sdk/README.md +172 -0
- package/package.json +539 -0
- package/scripts/build-info/resolvers.ts +51 -0
- package/scripts/capture-ax-fixture.ts +84 -0
- package/scripts/capture-login-fixture.ts +30 -0
- package/scripts/check-workflow-field-coverage.ts +101 -0
- package/scripts/extension-uat-harness.ts +340 -0
- package/scripts/format-prompts.ts +68 -0
- package/scripts/generate-api-spec-index.ts +580 -0
- package/scripts/generate-branding-index.ts +81 -0
- package/scripts/generate-build-info.ts +106 -0
- package/scripts/generate-console-catalog.ts +88 -0
- package/scripts/generate-console-field-metadata.ts +74 -0
- package/scripts/generate-docs-index.ts +40 -0
- package/scripts/generate-template.ts +32 -0
- package/scripts/generate-terraform-index.ts +81 -0
- package/scripts/mermaid-gallery.ts +132 -0
- package/scripts/uat-matrix-modalities.ts +451 -0
- package/scripts/uat-matrix-report.ts +175 -0
- package/scripts/uat-matrix.ts +353 -0
- package/src/async/index.ts +2 -0
- package/src/async/job-manager.ts +416 -0
- package/src/async/support.ts +5 -0
- package/src/autoresearch/apply-contract-to-state.ts +1 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +205 -0
- package/src/autoresearch/dashboard.ts +374 -0
- package/src/autoresearch/git.ts +191 -0
- package/src/autoresearch/helpers.ts +328 -0
- package/src/autoresearch/index.ts +402 -0
- package/src/autoresearch/prompt.md +236 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +303 -0
- package/src/autoresearch/tools/init-experiment.ts +400 -0
- package/src/autoresearch/tools/log-experiment.ts +793 -0
- package/src/autoresearch/tools/run-experiment.ts +679 -0
- package/src/autoresearch/types.ts +114 -0
- package/src/browser/acquire.ts +249 -0
- package/src/browser/actions.ts +126 -0
- package/src/browser/auth.ts +176 -0
- package/src/browser/ax.ts +134 -0
- package/src/browser/cdp-core.ts +45 -0
- package/src/browser/cdp-page-actions.ts +51 -0
- package/src/browser/chrome-locate.ts +66 -0
- package/src/browser/dom-context.ts +89 -0
- package/src/browser/dt-context.ts +100 -0
- package/src/browser/extension-bridge.ts +182 -0
- package/src/browser/extension-page-actions.ts +397 -0
- package/src/browser/extension-provider.ts +373 -0
- package/src/browser/index.ts +17 -0
- package/src/browser/input-commit.ts +74 -0
- package/src/browser/native-messaging.ts +20 -0
- package/src/browser/page-actions.ts +18 -0
- package/src/browser/provider.ts +155 -0
- package/src/browser/resolver.ts +126 -0
- package/src/browser/selector.ts +40 -0
- package/src/bun-imports.d.ts +22 -0
- package/src/capability/context-file.ts +43 -0
- package/src/capability/extension-module.ts +34 -0
- package/src/capability/extension.ts +47 -0
- package/src/capability/fs.ts +107 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +438 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +74 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +224 -0
- package/src/capability/settings.ts +34 -0
- package/src/capability/skill.ts +50 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/ssh.ts +41 -0
- package/src/capability/system-prompt.ts +34 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +168 -0
- package/src/cli/agents-cli.ts +138 -0
- package/src/cli/args.ts +286 -0
- package/src/cli/chrome-cli.ts +88 -0
- package/src/cli/classify-install-target.ts +50 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/config-cli.ts +418 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/grep-cli.ts +160 -0
- package/src/cli/grievances-cli.ts +78 -0
- package/src/cli/initial-message.ts +58 -0
- package/src/cli/jupyter-cli.ts +106 -0
- package/src/cli/list-models.ts +128 -0
- package/src/cli/plugin-cli.ts +964 -0
- package/src/cli/read-cli.ts +68 -0
- package/src/cli/session-picker.ts +52 -0
- package/src/cli/setup-cli.ts +479 -0
- package/src/cli/shell-cli.ts +174 -0
- package/src/cli/ssh-cli.ts +179 -0
- package/src/cli/stats-cli.ts +182 -0
- package/src/cli/update-cli.ts +446 -0
- package/src/cli/web-search-cli.ts +203 -0
- package/src/cli.ts +123 -0
- package/src/commands/agents.ts +57 -0
- package/src/commands/chrome-host.ts +71 -0
- package/src/commands/chrome.ts +28 -0
- package/src/commands/commit.ts +36 -0
- package/src/commands/config.ts +51 -0
- package/src/commands/grep.ts +48 -0
- package/src/commands/grievances.ts +20 -0
- package/src/commands/jupyter.ts +32 -0
- package/src/commands/launch.ts +144 -0
- package/src/commands/plugin.ts +78 -0
- package/src/commands/read.ts +33 -0
- package/src/commands/setup.ts +42 -0
- package/src/commands/shell.ts +29 -0
- package/src/commands/ssh.ts +60 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/update.ts +21 -0
- package/src/commands/web-search.ts +46 -0
- package/src/commit/agentic/agent.ts +314 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +354 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +25 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +38 -0
- package/src/commit/agentic/state.ts +60 -0
- package/src/commit/agentic/tools/analyze-file.ts +127 -0
- package/src/commit/agentic/tools/git-file-diff.ts +191 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +81 -0
- package/src/commit/agentic/tools/index.ts +54 -0
- package/src/commit/agentic/tools/propose-changelog.ts +139 -0
- package/src/commit/agentic/tools/propose-commit.ts +122 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/schemas.ts +31 -0
- package/src/commit/agentic/tools/split-commit.ts +251 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +183 -0
- package/src/commit/analysis/conventional.ts +122 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +105 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +40 -0
- package/src/commit/changelog/generate.ts +101 -0
- package/src/commit/changelog/index.ts +234 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +85 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +69 -0
- package/src/commit/map-reduce/map-phase.ts +193 -0
- package/src/commit/map-reduce/reduce-phase.ts +108 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +66 -0
- package/src/commit/pipeline.ts +242 -0
- package/src/commit/prompts/analysis-system.md +148 -0
- package/src/commit/prompts/analysis-user.md +38 -0
- package/src/commit/prompts/changelog-system.md +50 -0
- package/src/commit/prompts/changelog-user.md +18 -0
- package/src/commit/prompts/file-observer-system.md +24 -0
- package/src/commit/prompts/file-observer-user.md +8 -0
- package/src/commit/prompts/reduce-system.md +50 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +3 -0
- package/src/commit/prompts/summary-system.md +38 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/types.ts +118 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/commit/utils.ts +44 -0
- package/src/config/auto-config.ts +698 -0
- package/src/config/context-schema.json +115 -0
- package/src/config/file-lock.ts +121 -0
- package/src/config/keybindings.ts +493 -0
- package/src/config/mcp-schema.json +230 -0
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +2265 -0
- package/src/config/model-resolver.ts +1289 -0
- package/src/config/prompt-templates.ts +271 -0
- package/src/config/resolve-config-value.ts +110 -0
- package/src/config/settings-schema.ts +2155 -0
- package/src/config/settings.ts +730 -0
- package/src/config.ts +418 -0
- package/src/cursor.ts +323 -0
- package/src/dap/client.ts +674 -0
- package/src/dap/config.ts +150 -0
- package/src/dap/defaults.json +211 -0
- package/src/dap/index.ts +4 -0
- package/src/dap/session.ts +1255 -0
- package/src/dap/types.ts +600 -0
- package/src/debug/index.ts +440 -0
- package/src/debug/log-formatting.ts +58 -0
- package/src/debug/log-viewer.ts +908 -0
- package/src/debug/profiler.ts +158 -0
- package/src/debug/report-bundle.ts +365 -0
- package/src/debug/system-info.ts +107 -0
- package/src/discovery/agents-md.ts +67 -0
- package/src/discovery/agents.ts +219 -0
- package/src/discovery/builtin.ts +851 -0
- package/src/discovery/claude-plugins.ts +297 -0
- package/src/discovery/claude.ts +543 -0
- package/src/discovery/cline.ts +83 -0
- package/src/discovery/codex.ts +535 -0
- package/src/discovery/cursor.ts +220 -0
- package/src/discovery/gemini.ts +396 -0
- package/src/discovery/github.ts +118 -0
- package/src/discovery/helpers.ts +934 -0
- package/src/discovery/index.ts +79 -0
- package/src/discovery/language.ts +46 -0
- package/src/discovery/mcp-json.ts +171 -0
- package/src/discovery/opencode.ts +393 -0
- package/src/discovery/plugin-dir-roots.ts +28 -0
- package/src/discovery/ssh.ts +153 -0
- package/src/discovery/substitute-plugin-root.ts +29 -0
- package/src/discovery/vscode.ts +105 -0
- package/src/discovery/windsurf.ts +147 -0
- package/src/edit/diff.ts +818 -0
- package/src/edit/index.ts +481 -0
- package/src/edit/line-hash.ts +67 -0
- package/src/edit/modes/chunk.ts +762 -0
- package/src/edit/modes/hashline.ts +1366 -0
- package/src/edit/modes/patch.ts +1791 -0
- package/src/edit/modes/replace.ts +1115 -0
- package/src/edit/normalize.ts +375 -0
- package/src/edit/renderer.ts +627 -0
- package/src/exa/factory.ts +61 -0
- package/src/exa/index.ts +27 -0
- package/src/exa/mcp-client.ts +293 -0
- package/src/exa/render.ts +232 -0
- package/src/exa/researcher.ts +47 -0
- package/src/exa/search.ts +86 -0
- package/src/exa/types.ts +166 -0
- package/src/exa/websets.ts +247 -0
- package/src/exec/bash-executor.ts +332 -0
- package/src/exec/exec.ts +53 -0
- package/src/exec/idle-timeout-watchdog.ts +126 -0
- package/src/exec/non-interactive-env.ts +48 -0
- package/src/export/custom-share.ts +65 -0
- package/src/export/html/index.ts +161 -0
- package/src/export/html/template.css +1001 -0
- package/src/export/html/template.generated.ts +2 -0
- package/src/export/html/template.html +46 -0
- package/src/export/html/template.js +1950 -0
- package/src/export/html/template.macro.ts +24 -0
- package/src/export/html/vendor/highlight.min.js +1213 -0
- package/src/export/html/vendor/marked.min.js +6 -0
- package/src/export/ttsr.ts +434 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +25 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +456 -0
- package/src/extensibility/custom-commands/index.ts +2 -0
- package/src/extensibility/custom-commands/loader.ts +236 -0
- package/src/extensibility/custom-commands/types.ts +111 -0
- package/src/extensibility/custom-tools/index.ts +7 -0
- package/src/extensibility/custom-tools/loader.ts +238 -0
- package/src/extensibility/custom-tools/types.ts +251 -0
- package/src/extensibility/custom-tools/wrapper.ts +47 -0
- package/src/extensibility/extensions/index.ts +15 -0
- package/src/extensibility/extensions/loader.ts +572 -0
- package/src/extensibility/extensions/runner.ts +848 -0
- package/src/extensibility/extensions/types.ts +1394 -0
- package/src/extensibility/extensions/wrapper.ts +191 -0
- package/src/extensibility/hooks/index.ts +5 -0
- package/src/extensibility/hooks/loader.ts +255 -0
- package/src/extensibility/hooks/runner.ts +425 -0
- package/src/extensibility/hooks/tool-wrapper.ts +108 -0
- package/src/extensibility/hooks/types.ts +826 -0
- package/src/extensibility/plugins/doctor.ts +66 -0
- package/src/extensibility/plugins/git-url.ts +281 -0
- package/src/extensibility/plugins/index.ts +9 -0
- package/src/extensibility/plugins/installer.ts +192 -0
- package/src/extensibility/plugins/loader.ts +267 -0
- package/src/extensibility/plugins/manager.ts +731 -0
- package/src/extensibility/plugins/marketplace/cache.ts +136 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +318 -0
- package/src/extensibility/plugins/marketplace/index.ts +6 -0
- package/src/extensibility/plugins/marketplace/manager.ts +781 -0
- package/src/extensibility/plugins/marketplace/prerequisites.ts +255 -0
- package/src/extensibility/plugins/marketplace/registry.ts +196 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
- package/src/extensibility/plugins/marketplace/types.ts +201 -0
- package/src/extensibility/plugins/parser.ts +105 -0
- package/src/extensibility/plugins/types.ts +194 -0
- package/src/extensibility/skills.ts +269 -0
- package/src/extensibility/slash-commands.ts +246 -0
- package/src/extensibility/tool-proxy.ts +25 -0
- package/src/extensibility/utils.ts +38 -0
- package/src/index.ts +61 -0
- package/src/internal-urls/agent-protocol.ts +136 -0
- package/src/internal-urls/api-catalog-resolve.ts +495 -0
- package/src/internal-urls/api-catalog-types.ts +94 -0
- package/src/internal-urls/api-spec-resolve.ts +1036 -0
- package/src/internal-urls/api-spec-types.ts +239 -0
- package/src/internal-urls/artifact-protocol.ts +97 -0
- package/src/internal-urls/branding-index.generated.ts +88 -0
- package/src/internal-urls/build-info-runtime.ts +302 -0
- package/src/internal-urls/build-info.generated.ts +33 -0
- package/src/internal-urls/computer-profile.ts +692 -0
- package/src/internal-urls/console-catalog-types.ts +21 -0
- package/src/internal-urls/console-catalog.generated.ts +532 -0
- package/src/internal-urls/console-field-metadata-types.ts +59 -0
- package/src/internal-urls/console-field-metadata.generated.ts +4841 -0
- package/src/internal-urls/console-resolve.ts +178 -0
- package/src/internal-urls/docs-index.generated.ts +698 -0
- package/src/internal-urls/extension-api.md +147 -0
- package/src/internal-urls/index.ts +46 -0
- package/src/internal-urls/jobs-protocol.ts +119 -0
- package/src/internal-urls/json-query.ts +126 -0
- package/src/internal-urls/local-protocol.ts +206 -0
- package/src/internal-urls/mcp-protocol.ts +156 -0
- package/src/internal-urls/memory-protocol.ts +133 -0
- package/src/internal-urls/parse.ts +72 -0
- package/src/internal-urls/profile-collectors.ts +106 -0
- package/src/internal-urls/router.ts +55 -0
- package/src/internal-urls/rule-protocol.ts +55 -0
- package/src/internal-urls/skill-protocol.ts +111 -0
- package/src/internal-urls/terraform-index.generated.ts +1852 -0
- package/src/internal-urls/terraform-resolve.ts +184 -0
- package/src/internal-urls/terraform-types.ts +45 -0
- package/src/internal-urls/types.ts +52 -0
- package/src/internal-urls/user-profile.ts +363 -0
- package/src/internal-urls/xcsh-protocol.ts +644 -0
- package/src/ipy/cancellation.ts +28 -0
- package/src/ipy/executor.ts +1155 -0
- package/src/ipy/gateway-coordinator.ts +424 -0
- package/src/ipy/kernel.ts +1091 -0
- package/src/ipy/modules.ts +144 -0
- package/src/ipy/prelude.py +849 -0
- package/src/ipy/prelude.ts +3 -0
- package/src/ipy/runtime.ts +221 -0
- package/src/locales/ar.json +512 -0
- package/src/locales/de.json +512 -0
- package/src/locales/en.json +549 -0
- package/src/locales/es.json +512 -0
- package/src/locales/fr.json +512 -0
- package/src/locales/hi.json +512 -0
- package/src/locales/index.ts +29 -0
- package/src/locales/it.json +512 -0
- package/src/locales/ja.json +512 -0
- package/src/locales/ko.json +512 -0
- package/src/locales/pt-br.json +512 -0
- package/src/locales/th.json +512 -0
- package/src/locales/zh-cn.json +512 -0
- package/src/locales/zh-tw.json +512 -0
- package/src/lsp/client.ts +941 -0
- package/src/lsp/clients/biome-client.ts +202 -0
- package/src/lsp/clients/index.ts +50 -0
- package/src/lsp/clients/lsp-linter-client.ts +93 -0
- package/src/lsp/clients/swiftlint-client.ts +120 -0
- package/src/lsp/config.ts +418 -0
- package/src/lsp/defaults.json +999 -0
- package/src/lsp/edits.ts +109 -0
- package/src/lsp/index.ts +1746 -0
- package/src/lsp/lspmux.ts +233 -0
- package/src/lsp/render.ts +692 -0
- package/src/lsp/startup-events.ts +13 -0
- package/src/lsp/types.ts +444 -0
- package/src/lsp/utils.ts +691 -0
- package/src/main.ts +944 -0
- package/src/mcp/client.ts +482 -0
- package/src/mcp/config-writer.ts +225 -0
- package/src/mcp/config.ts +365 -0
- package/src/mcp/discoverable-tool-metadata.ts +202 -0
- package/src/mcp/index.ts +29 -0
- package/src/mcp/json-rpc.ts +84 -0
- package/src/mcp/loader.ts +124 -0
- package/src/mcp/manager.ts +1152 -0
- package/src/mcp/oauth-discovery.ts +349 -0
- package/src/mcp/oauth-flow.ts +387 -0
- package/src/mcp/render.ts +157 -0
- package/src/mcp/smithery-auth.ts +104 -0
- package/src/mcp/smithery-connect.ts +145 -0
- package/src/mcp/smithery-registry.ts +477 -0
- package/src/mcp/tool-bridge.ts +417 -0
- package/src/mcp/tool-cache.ts +117 -0
- package/src/mcp/transports/http.ts +475 -0
- package/src/mcp/transports/index.ts +6 -0
- package/src/mcp/transports/stdio.ts +325 -0
- package/src/mcp/types.ts +423 -0
- package/src/memories/index.ts +1115 -0
- package/src/memories/storage.ts +577 -0
- package/src/modes/acp/acp-agent.ts +1361 -0
- package/src/modes/acp/acp-event-mapper.ts +538 -0
- package/src/modes/acp/acp-mode.ts +15 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +1124 -0
- package/src/modes/components/assistant-message.ts +213 -0
- package/src/modes/components/bash-execution.ts +345 -0
- package/src/modes/components/bordered-loader.ts +41 -0
- package/src/modes/components/branch-summary-message.ts +45 -0
- package/src/modes/components/btw-panel.ts +103 -0
- package/src/modes/components/compaction-summary-message.ts +51 -0
- package/src/modes/components/context-add-wizard.ts +534 -0
- package/src/modes/components/countdown-timer.ts +75 -0
- package/src/modes/components/custom-editor.ts +238 -0
- package/src/modes/components/custom-message.ts +91 -0
- package/src/modes/components/diff.ts +210 -0
- package/src/modes/components/dynamic-border.ts +25 -0
- package/src/modes/components/extensions/extension-dashboard.ts +326 -0
- package/src/modes/components/extensions/extension-list.ts +492 -0
- package/src/modes/components/extensions/index.ts +9 -0
- package/src/modes/components/extensions/inspector-panel.ts +317 -0
- package/src/modes/components/extensions/state-manager.ts +587 -0
- package/src/modes/components/extensions/types.ts +191 -0
- package/src/modes/components/footer.ts +262 -0
- package/src/modes/components/gutter-block.ts +315 -0
- package/src/modes/components/history-search.ts +158 -0
- package/src/modes/components/hook-editor.ts +151 -0
- package/src/modes/components/hook-input.ts +81 -0
- package/src/modes/components/hook-message.ts +100 -0
- package/src/modes/components/hook-selector.ts +192 -0
- package/src/modes/components/index.ts +36 -0
- package/src/modes/components/keybinding-hints.ts +65 -0
- package/src/modes/components/login-dialog.ts +164 -0
- package/src/modes/components/mcp-add-wizard.ts +1314 -0
- package/src/modes/components/model-selector.ts +946 -0
- package/src/modes/components/oauth-selector.ts +211 -0
- package/src/modes/components/plugin-selector.ts +95 -0
- package/src/modes/components/plugin-settings.ts +477 -0
- package/src/modes/components/plugins/index.ts +5 -0
- package/src/modes/components/plugins/plugin-dashboard.ts +523 -0
- package/src/modes/components/plugins/plugin-inspector-pane.ts +84 -0
- package/src/modes/components/plugins/plugin-list-pane.ts +109 -0
- package/src/modes/components/plugins/state-manager.ts +229 -0
- package/src/modes/components/plugins/types.ts +51 -0
- package/src/modes/components/plugins/utils.ts +6 -0
- package/src/modes/components/python-execution.ts +244 -0
- package/src/modes/components/queue-mode-selector.ts +56 -0
- package/src/modes/components/read-tool-group.ts +137 -0
- package/src/modes/components/session-observer-overlay.ts +483 -0
- package/src/modes/components/session-selector.ts +343 -0
- package/src/modes/components/settings-defs.ts +568 -0
- package/src/modes/components/settings-selector.ts +641 -0
- package/src/modes/components/show-images-selector.ts +45 -0
- package/src/modes/components/skill-message.ts +90 -0
- package/src/modes/components/status-line/context-gradient.ts +64 -0
- package/src/modes/components/status-line/git-utils.ts +42 -0
- package/src/modes/components/status-line/hex-ansi.ts +29 -0
- package/src/modes/components/status-line/index.ts +4 -0
- package/src/modes/components/status-line/presets.ts +174 -0
- package/src/modes/components/status-line/segments.ts +494 -0
- package/src/modes/components/status-line/separators.ts +55 -0
- package/src/modes/components/status-line/token-rate.ts +66 -0
- package/src/modes/components/status-line/types.ts +96 -0
- package/src/modes/components/status-line-segment-editor.ts +361 -0
- package/src/modes/components/status-line.ts +729 -0
- package/src/modes/components/theme-selector.ts +63 -0
- package/src/modes/components/thinking-selector.ts +52 -0
- package/src/modes/components/todo-reminder.ts +54 -0
- package/src/modes/components/tool-execution.ts +781 -0
- package/src/modes/components/tree-selector.ts +908 -0
- package/src/modes/components/ttsr-notification.ts +80 -0
- package/src/modes/components/user-message-selector.ts +141 -0
- package/src/modes/components/user-message.ts +100 -0
- package/src/modes/components/visual-truncate.ts +63 -0
- package/src/modes/components/welcome-checks.ts +313 -0
- package/src/modes/components/welcome.ts +270 -0
- package/src/modes/controllers/btw-controller.ts +193 -0
- package/src/modes/controllers/chord-routing.ts +37 -0
- package/src/modes/controllers/command-controller.ts +1293 -0
- package/src/modes/controllers/context-command-controller.ts +61 -0
- package/src/modes/controllers/event-controller.ts +743 -0
- package/src/modes/controllers/extension-ui-controller.ts +983 -0
- package/src/modes/controllers/input-controller.ts +877 -0
- package/src/modes/controllers/mcp-command-controller.ts +1940 -0
- package/src/modes/controllers/selector-controller.ts +1292 -0
- package/src/modes/controllers/ssh-command-controller.ts +421 -0
- package/src/modes/index.ts +34 -0
- package/src/modes/interactive-mode.ts +1781 -0
- package/src/modes/oauth-manual-input.ts +42 -0
- package/src/modes/print-mode.ts +196 -0
- package/src/modes/prompt-action-autocomplete.ts +231 -0
- package/src/modes/rpc/host-tools.ts +186 -0
- package/src/modes/rpc/rpc-client.ts +762 -0
- package/src/modes/rpc/rpc-mode.ts +877 -0
- package/src/modes/rpc/rpc-types.ts +322 -0
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +30 -0
- package/src/modes/theme/defaults/index.ts +7 -0
- package/src/modes/theme/defaults/xcsh-dark.json +113 -0
- package/src/modes/theme/defaults/xcsh-light.json +115 -0
- package/src/modes/theme/mermaid-cache.ts +151 -0
- package/src/modes/theme/mermaid-palette.ts +39 -0
- package/src/modes/theme/theme-schema.json +553 -0
- package/src/modes/theme/theme.ts +2563 -0
- package/src/modes/types.ts +273 -0
- package/src/modes/utils/hotkeys-markdown.ts +60 -0
- package/src/modes/utils/keybinding-matchers.ts +30 -0
- package/src/modes/utils/read-group-outcome-aggregator.ts +55 -0
- package/src/modes/utils/sanitize-error-message.ts +60 -0
- package/src/modes/utils/tools-markdown.ts +28 -0
- package/src/modes/utils/ui-helpers.ts +734 -0
- package/src/plan-mode/approved-plan.ts +55 -0
- package/src/plan-mode/state.ts +6 -0
- package/src/priority.json +37 -0
- package/src/prompts/agents/designer.md +67 -0
- package/src/prompts/agents/explore.md +61 -0
- package/src/prompts/agents/frontmatter.md +10 -0
- package/src/prompts/agents/init.md +36 -0
- package/src/prompts/agents/librarian.md +121 -0
- package/src/prompts/agents/plan.md +49 -0
- package/src/prompts/agents/reviewer.md +127 -0
- package/src/prompts/agents/task.md +16 -0
- package/src/prompts/ci-green-request.md +35 -0
- package/src/prompts/compaction/branch-summary-context.md +5 -0
- package/src/prompts/compaction/branch-summary-preamble.md +2 -0
- package/src/prompts/compaction/branch-summary.md +30 -0
- package/src/prompts/compaction/compaction-short-summary.md +9 -0
- package/src/prompts/compaction/compaction-summary-context.md +5 -0
- package/src/prompts/compaction/compaction-summary.md +38 -0
- package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
- package/src/prompts/compaction/compaction-update-summary.md +45 -0
- package/src/prompts/memories/consolidation.md +30 -0
- package/src/prompts/memories/read-path.md +11 -0
- package/src/prompts/memories/stage_one_input.md +6 -0
- package/src/prompts/memories/stage_one_system.md +21 -0
- package/src/prompts/review-request.md +70 -0
- package/src/prompts/system/agent-creation-architect.md +65 -0
- package/src/prompts/system/agent-creation-user.md +6 -0
- package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/commit-message-system.md +2 -0
- package/src/prompts/system/custom-system-prompt.md +82 -0
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/system/file-operations.md +10 -0
- package/src/prompts/system/handoff-document.md +48 -0
- package/src/prompts/system/plan-mode-active.md +114 -0
- package/src/prompts/system/plan-mode-approved.md +23 -0
- package/src/prompts/system/plan-mode-reference.md +14 -0
- package/src/prompts/system/plan-mode-subagent.md +34 -0
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/system/subagent-submit-reminder.md +7 -0
- package/src/prompts/system/subagent-system-prompt.md +36 -0
- package/src/prompts/system/subagent-user-prompt.md +12 -0
- package/src/prompts/system/summarization-system.md +3 -0
- package/src/prompts/system/system-prompt.md +698 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/prompts/system/web-search.md +28 -0
- package/src/prompts/tools/ask.md +28 -0
- package/src/prompts/tools/ast-edit.md +48 -0
- package/src/prompts/tools/ast-grep.md +54 -0
- package/src/prompts/tools/async-result.md +5 -0
- package/src/prompts/tools/bash.md +67 -0
- package/src/prompts/tools/browser.md +26 -0
- package/src/prompts/tools/calculator.md +10 -0
- package/src/prompts/tools/cancel-job.md +5 -0
- package/src/prompts/tools/catalog-workflow-runner.md +13 -0
- package/src/prompts/tools/checkpoint.md +16 -0
- package/src/prompts/tools/chunk-edit.md +279 -0
- package/src/prompts/tools/debug.md +43 -0
- package/src/prompts/tools/display-image.md +23 -0
- package/src/prompts/tools/exit-plan-mode.md +41 -0
- package/src/prompts/tools/find.md +26 -0
- package/src/prompts/tools/gemini-image.md +7 -0
- package/src/prompts/tools/grep.md +29 -0
- package/src/prompts/tools/hashline.md +131 -0
- package/src/prompts/tools/inspect-image-system.md +20 -0
- package/src/prompts/tools/inspect-image.md +32 -0
- package/src/prompts/tools/lsp.md +33 -0
- package/src/prompts/tools/patch.md +77 -0
- package/src/prompts/tools/poll.md +5 -0
- package/src/prompts/tools/python.md +60 -0
- package/src/prompts/tools/read-chunk.md +35 -0
- package/src/prompts/tools/read.md +63 -0
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/prompts/tools/replace.md +36 -0
- package/src/prompts/tools/resolve.md +8 -0
- package/src/prompts/tools/rewind.md +13 -0
- package/src/prompts/tools/search-tool-bm25.md +34 -0
- package/src/prompts/tools/ssh.md +44 -0
- package/src/prompts/tools/task-summary.md +28 -0
- package/src/prompts/tools/task.md +131 -0
- package/src/prompts/tools/todo-write.md +89 -0
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +14 -0
- package/src/prompts/tools/xcsh-api.md +283 -0
- package/src/resource-management/index.ts +42 -0
- package/src/sdk.ts +2123 -0
- package/src/secrets/index.ts +146 -0
- package/src/secrets/obfuscator.ts +290 -0
- package/src/secrets/regex.ts +21 -0
- package/src/services/context-env.ts +102 -0
- package/src/services/xcsh-api-client.ts +230 -0
- package/src/services/xcsh-context-command.ts +803 -0
- package/src/services/xcsh-context-display.ts +49 -0
- package/src/services/xcsh-context-indicators.ts +17 -0
- package/src/services/xcsh-context-segment.ts +39 -0
- package/src/services/xcsh-context.ts +1410 -0
- package/src/services/xcsh-env.ts +107 -0
- package/src/services/xcsh-knowledge.ts +177 -0
- package/src/services/xcsh-table.ts +164 -0
- package/src/session/agent-session.ts +6690 -0
- package/src/session/agent-storage.ts +451 -0
- package/src/session/artifacts.ts +132 -0
- package/src/session/auth-storage.ts +17 -0
- package/src/session/blob-store.ts +135 -0
- package/src/session/compaction/branch-summarization.ts +322 -0
- package/src/session/compaction/compaction.ts +1388 -0
- package/src/session/compaction/index.ts +7 -0
- package/src/session/compaction/pruning.ts +91 -0
- package/src/session/compaction/utils.ts +184 -0
- package/src/session/history-storage.ts +253 -0
- package/src/session/messages.ts +597 -0
- package/src/session/session-dump-format.ts +203 -0
- package/src/session/session-manager.ts +2816 -0
- package/src/session/session-storage.ts +370 -0
- package/src/session/streaming-output.ts +770 -0
- package/src/session/tool-choice-queue.ts +213 -0
- package/src/slash-commands/builtin-registry.ts +1614 -0
- package/src/slash-commands/export-command.ts +178 -0
- package/src/slash-commands/marketplace-install-parser.ts +99 -0
- package/src/slash-commands/resource-commands.ts +250 -0
- package/src/ssh/config-writer.ts +183 -0
- package/src/ssh/connection-manager.ts +445 -0
- package/src/ssh/ssh-executor.ts +131 -0
- package/src/ssh/sshfs-mount.ts +137 -0
- package/src/ssh/utils.ts +8 -0
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/system-prompt.ts +688 -0
- package/src/task/agents.ts +166 -0
- package/src/task/commands.ts +131 -0
- package/src/task/discovery.ts +126 -0
- package/src/task/executor.ts +1324 -0
- package/src/task/index.ts +1197 -0
- package/src/task/isolation-backend.ts +72 -0
- package/src/task/name-generator.ts +1577 -0
- package/src/task/output-manager.ts +107 -0
- package/src/task/parallel.ts +116 -0
- package/src/task/render.ts +1035 -0
- package/src/task/subprocess-tool-registry.ts +88 -0
- package/src/task/template.ts +33 -0
- package/src/task/types.ts +230 -0
- package/src/task/worktree.ts +580 -0
- package/src/task/xcsh-command.ts +26 -0
- package/src/thinking.ts +87 -0
- package/src/tools/action-renderer.ts +198 -0
- package/src/tools/archive-reader.ts +315 -0
- package/src/tools/ask.ts +791 -0
- package/src/tools/ast-edit.ts +492 -0
- package/src/tools/ast-grep.ts +453 -0
- package/src/tools/auto-generated-guard.ts +305 -0
- package/src/tools/bash-interactive.ts +383 -0
- package/src/tools/bash-interceptor.ts +67 -0
- package/src/tools/bash-normalize.ts +107 -0
- package/src/tools/bash-skill-urls.ts +246 -0
- package/src/tools/bash.ts +878 -0
- package/src/tools/browser-renderer.ts +129 -0
- package/src/tools/browser.ts +1366 -0
- package/src/tools/calculator.ts +543 -0
- package/src/tools/cancel-job.ts +100 -0
- package/src/tools/catalog-workflow-runner.ts +718 -0
- package/src/tools/checkpoint.ts +133 -0
- package/src/tools/context.ts +39 -0
- package/src/tools/debug.ts +1004 -0
- package/src/tools/display-image-renderer.ts +96 -0
- package/src/tools/display-image.ts +109 -0
- package/src/tools/exit-plan-mode.ts +101 -0
- package/src/tools/fetch.ts +1462 -0
- package/src/tools/find.ts +437 -0
- package/src/tools/fs-cache-invalidation.ts +33 -0
- package/src/tools/gemini-image.ts +1004 -0
- package/src/tools/grep.ts +705 -0
- package/src/tools/index.ts +442 -0
- package/src/tools/inspect-image-renderer.ts +101 -0
- package/src/tools/inspect-image.ts +176 -0
- package/src/tools/json-tree.ts +266 -0
- package/src/tools/jtd-to-json-schema.ts +199 -0
- package/src/tools/jtd-to-typescript.ts +136 -0
- package/src/tools/jtd-utils.ts +102 -0
- package/src/tools/list-limit.ts +40 -0
- package/src/tools/mermaid-renderer.ts +127 -0
- package/src/tools/notebook.ts +288 -0
- package/src/tools/output-meta.ts +588 -0
- package/src/tools/path-utils.ts +560 -0
- package/src/tools/plan-mode-guard.ts +41 -0
- package/src/tools/poll-tool.ts +177 -0
- package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
- package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
- package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
- package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
- package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
- package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
- package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
- package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
- package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
- package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
- package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
- package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
- package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
- package/src/tools/python.ts +1184 -0
- package/src/tools/read.ts +1591 -0
- package/src/tools/render-mermaid.ts +93 -0
- package/src/tools/render-utils.ts +677 -0
- package/src/tools/renderers.ts +82 -0
- package/src/tools/report-tool-issue.ts +80 -0
- package/src/tools/resolve.ts +196 -0
- package/src/tools/review.ts +238 -0
- package/src/tools/search-tool-bm25.ts +288 -0
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/ssh.ts +322 -0
- package/src/tools/submit-result.ts +229 -0
- package/src/tools/todo-render.ts +33 -0
- package/src/tools/todo-write.ts +440 -0
- package/src/tools/tool-errors.ts +62 -0
- package/src/tools/tool-result.ts +93 -0
- package/src/tools/tool-timeouts.ts +30 -0
- package/src/tools/vim.ts +981 -0
- package/src/tools/write.ts +671 -0
- package/src/tools/xcsh-api-renderer.ts +402 -0
- package/src/tools/xcsh-api.ts +887 -0
- package/src/tui/code-cell.ts +101 -0
- package/src/tui/file-list.ts +47 -0
- package/src/tui/index.ts +11 -0
- package/src/tui/output-block.ts +175 -0
- package/src/tui/status-line.ts +39 -0
- package/src/tui/tree-list.ts +84 -0
- package/src/tui/types.ts +15 -0
- package/src/tui/utils.ts +103 -0
- package/src/utils/changelog.ts +98 -0
- package/src/utils/clipboard.ts +80 -0
- package/src/utils/command-args.ts +76 -0
- package/src/utils/commit-message-generator.ts +137 -0
- package/src/utils/edit-mode.ts +50 -0
- package/src/utils/event-bus.ts +33 -0
- package/src/utils/external-editor.ts +65 -0
- package/src/utils/file-display-mode.ts +37 -0
- package/src/utils/file-mentions.ts +376 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/git.ts +1495 -0
- package/src/utils/gitstatus.ts +140 -0
- package/src/utils/image-convert.ts +27 -0
- package/src/utils/image-loading.ts +98 -0
- package/src/utils/image-passthrough.ts +118 -0
- package/src/utils/image-resize.ts +272 -0
- package/src/utils/image-viewer.ts +17 -0
- package/src/utils/lang-from-path.ts +239 -0
- package/src/utils/markit.ts +81 -0
- package/src/utils/open.ts +20 -0
- package/src/utils/session-color.ts +55 -0
- package/src/utils/shell-snapshot.ts +188 -0
- package/src/utils/title-generator.ts +193 -0
- package/src/utils/tool-choice.ts +28 -0
- package/src/utils/tools-manager.ts +356 -0
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
- package/src/web/kagi.ts +174 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/artifacthub.ts +207 -0
- package/src/web/scrapers/arxiv.ts +83 -0
- package/src/web/scrapers/aur.ts +162 -0
- package/src/web/scrapers/biorxiv.ts +133 -0
- package/src/web/scrapers/bluesky.ts +262 -0
- package/src/web/scrapers/brew.ts +172 -0
- package/src/web/scrapers/cheatsh.ts +68 -0
- package/src/web/scrapers/chocolatey.ts +196 -0
- package/src/web/scrapers/choosealicense.ts +95 -0
- package/src/web/scrapers/cisa-kev.ts +87 -0
- package/src/web/scrapers/clojars.ts +154 -0
- package/src/web/scrapers/coingecko.ts +177 -0
- package/src/web/scrapers/crates-io.ts +110 -0
- package/src/web/scrapers/crossref.ts +136 -0
- package/src/web/scrapers/devto.ts +147 -0
- package/src/web/scrapers/discogs.ts +306 -0
- package/src/web/scrapers/discourse.ts +197 -0
- package/src/web/scrapers/dockerhub.ts +138 -0
- package/src/web/scrapers/docs-rs.ts +653 -0
- package/src/web/scrapers/fdroid.ts +134 -0
- package/src/web/scrapers/firefox-addons.ts +191 -0
- package/src/web/scrapers/flathub.ts +223 -0
- package/src/web/scrapers/github-gist.ts +58 -0
- package/src/web/scrapers/github.ts +452 -0
- package/src/web/scrapers/gitlab.ts +401 -0
- package/src/web/scrapers/go-pkg.ts +266 -0
- package/src/web/scrapers/hackage.ts +140 -0
- package/src/web/scrapers/hackernews.ts +189 -0
- package/src/web/scrapers/hex.ts +105 -0
- package/src/web/scrapers/huggingface.ts +321 -0
- package/src/web/scrapers/iacr.ts +89 -0
- package/src/web/scrapers/index.ts +252 -0
- package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
- package/src/web/scrapers/lemmy.ts +203 -0
- package/src/web/scrapers/lobsters.ts +175 -0
- package/src/web/scrapers/mastodon.ts +292 -0
- package/src/web/scrapers/maven.ts +138 -0
- package/src/web/scrapers/mdn.ts +169 -0
- package/src/web/scrapers/metacpan.ts +222 -0
- package/src/web/scrapers/musicbrainz.ts +250 -0
- package/src/web/scrapers/npm.ts +98 -0
- package/src/web/scrapers/nuget.ts +183 -0
- package/src/web/scrapers/nvd.ts +222 -0
- package/src/web/scrapers/ollama.ts +239 -0
- package/src/web/scrapers/open-vsx.ts +106 -0
- package/src/web/scrapers/opencorporates.ts +292 -0
- package/src/web/scrapers/openlibrary.ts +336 -0
- package/src/web/scrapers/orcid.ts +286 -0
- package/src/web/scrapers/osv.ts +176 -0
- package/src/web/scrapers/packagist.ts +160 -0
- package/src/web/scrapers/pub-dev.ts +143 -0
- package/src/web/scrapers/pubmed.ts +211 -0
- package/src/web/scrapers/pypi.ts +112 -0
- package/src/web/scrapers/rawg.ts +110 -0
- package/src/web/scrapers/readthedocs.ts +120 -0
- package/src/web/scrapers/reddit.ts +95 -0
- package/src/web/scrapers/repology.ts +251 -0
- package/src/web/scrapers/rfc.ts +201 -0
- package/src/web/scrapers/rubygems.ts +103 -0
- package/src/web/scrapers/searchcode.ts +190 -0
- package/src/web/scrapers/sec-edgar.ts +261 -0
- package/src/web/scrapers/semantic-scholar.ts +171 -0
- package/src/web/scrapers/snapcraft.ts +187 -0
- package/src/web/scrapers/sourcegraph.ts +336 -0
- package/src/web/scrapers/spdx.ts +108 -0
- package/src/web/scrapers/spotify.ts +198 -0
- package/src/web/scrapers/stackoverflow.ts +120 -0
- package/src/web/scrapers/terraform.ts +277 -0
- package/src/web/scrapers/tldr.ts +47 -0
- package/src/web/scrapers/twitter.ts +93 -0
- package/src/web/scrapers/types.ts +267 -0
- package/src/web/scrapers/utils.ts +109 -0
- package/src/web/scrapers/vimeo.ts +133 -0
- package/src/web/scrapers/vscode-marketplace.ts +187 -0
- package/src/web/scrapers/w3c.ts +156 -0
- package/src/web/scrapers/wikidata.ts +344 -0
- package/src/web/scrapers/wikipedia.ts +84 -0
- package/src/web/scrapers/youtube.ts +313 -0
- package/src/web/search/errors.ts +120 -0
- package/src/web/search/index.ts +346 -0
- package/src/web/search/params.ts +129 -0
- package/src/web/search/provider.ts +97 -0
- package/src/web/search/providers/anthropic.ts +339 -0
- package/src/web/search/providers/base.ts +35 -0
- package/src/web/search/providers/brave.ts +150 -0
- package/src/web/search/providers/codex.ts +402 -0
- package/src/web/search/providers/exa.ts +264 -0
- package/src/web/search/providers/firecrawl.ts +115 -0
- package/src/web/search/providers/gemini.ts +566 -0
- package/src/web/search/providers/jina.ts +99 -0
- package/src/web/search/providers/kagi.ts +70 -0
- package/src/web/search/providers/kimi.ts +157 -0
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/providers/perplexity.ts +546 -0
- package/src/web/search/providers/synthetic.ts +113 -0
- package/src/web/search/providers/tavily.ts +162 -0
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +311 -0
- package/src/web/search/render.ts +348 -0
- package/src/web/search/types.ts +438 -0
- package/src/web/search/utils.ts +17 -0
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ContextResolver,
|
|
5
|
+
getXCSHConfigDir,
|
|
6
|
+
logger,
|
|
7
|
+
RESERVED_CONTEXT_NAMES,
|
|
8
|
+
xcshContextPaths,
|
|
9
|
+
} from "@f5xc-salesdemos/pi-utils";
|
|
10
|
+
import { Settings } from "../config/settings";
|
|
11
|
+
import { SECRET_ENV_PATTERNS } from "../secrets/index";
|
|
12
|
+
import { XCSHApiClient } from "./xcsh-api-client";
|
|
13
|
+
import {
|
|
14
|
+
deriveTenantFromUrl,
|
|
15
|
+
hasEnvOverride,
|
|
16
|
+
isInjectableContextEnvKey,
|
|
17
|
+
normalizeApiUrl,
|
|
18
|
+
RESERVED_ENV_KEYS,
|
|
19
|
+
RESERVED_ENV_MESSAGES,
|
|
20
|
+
XCSH_API_TOKEN,
|
|
21
|
+
XCSH_API_URL,
|
|
22
|
+
XCSH_CONTEXT_NAME,
|
|
23
|
+
XCSH_NAMESPACE,
|
|
24
|
+
XCSH_TENANT,
|
|
25
|
+
} from "./xcsh-env";
|
|
26
|
+
|
|
27
|
+
export const CURRENT_SCHEMA_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
export const CURRENT_EXPORT_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
export interface ExportBundle {
|
|
32
|
+
/** Export format version — distinct from per-context XCSHContext.version (schema version). */
|
|
33
|
+
version: number;
|
|
34
|
+
exportedAt: string;
|
|
35
|
+
/** When true, importContexts rejects this bundle. */
|
|
36
|
+
tokensMasked: boolean;
|
|
37
|
+
/** Same shape as on-disk context JSON. Tokens masked iff tokensMasked=true. */
|
|
38
|
+
contexts: XCSHContext[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ImportResult {
|
|
42
|
+
imported: string[];
|
|
43
|
+
overwritten: string[];
|
|
44
|
+
skipped: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface KnowledgeSource {
|
|
48
|
+
url: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
type?: "llms-txt" | "skill-dir" | "docs-site";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface XCSHContext {
|
|
54
|
+
name: string;
|
|
55
|
+
apiUrl: string;
|
|
56
|
+
apiToken: string;
|
|
57
|
+
defaultNamespace: string;
|
|
58
|
+
env?: Record<string, string>;
|
|
59
|
+
/** Env var names from `env` whose values should be masked in output (e.g. ["XCSH_USERNAME"]). */
|
|
60
|
+
sensitiveKeys?: string[];
|
|
61
|
+
knowledgeSources?: KnowledgeSource[];
|
|
62
|
+
includeSkills?: string[];
|
|
63
|
+
excludeSkills?: string[];
|
|
64
|
+
version?: number;
|
|
65
|
+
metadata?: {
|
|
66
|
+
createdAt?: string;
|
|
67
|
+
expiresAt?: string;
|
|
68
|
+
lastRotatedAt?: string;
|
|
69
|
+
rotateAfterDays?: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type AuthStatus = "connected" | "auth_error" | "offline" | "unknown";
|
|
74
|
+
|
|
75
|
+
export type TokenHealth = "ok" | "expiring" | "expired";
|
|
76
|
+
|
|
77
|
+
export interface ContextStatus {
|
|
78
|
+
activeContextName: string | null;
|
|
79
|
+
activeContextUrl: string | null;
|
|
80
|
+
activeContextTenant: string | null;
|
|
81
|
+
activeContextNamespace: string | null;
|
|
82
|
+
credentialSource: "context" | "environment" | "mixed" | "none";
|
|
83
|
+
authStatus: AuthStatus;
|
|
84
|
+
isConfigured: boolean;
|
|
85
|
+
/** Milliseconds measured by the most recent validateToken() call. Absent if validateToken has not run. */
|
|
86
|
+
authLatencyMs?: number;
|
|
87
|
+
/** Epoch ms of the most recent validateToken() call. Absent if validateToken has not run. */
|
|
88
|
+
authCheckedAt?: number;
|
|
89
|
+
tokenHealth?: TokenHealth;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Result of validating credentials for a named context without activating it.
|
|
94
|
+
* Returned by `ContextService.validateContextByName()`. Callers get the full
|
|
95
|
+
* context back rather than correlating by name so rendering code can use a
|
|
96
|
+
* single object (tenant, URL, masked token, status) without a second lookup.
|
|
97
|
+
*
|
|
98
|
+
* Auth failure is carried here as `status: "auth_error" | "offline"` with
|
|
99
|
+
* optional `errorClass` — not thrown. The method throws only for missing /
|
|
100
|
+
* invalid-name / incompatible-version cases.
|
|
101
|
+
*/
|
|
102
|
+
export interface ValidationResult {
|
|
103
|
+
context: XCSHContext;
|
|
104
|
+
status: AuthStatus;
|
|
105
|
+
latencyMs?: number;
|
|
106
|
+
errorClass?: "network" | "credential" | "url_not_found";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class ContextError extends Error {
|
|
110
|
+
constructor(
|
|
111
|
+
message: string,
|
|
112
|
+
readonly contextName?: string,
|
|
113
|
+
) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "ContextError";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class ContextService {
|
|
120
|
+
static #instance: ContextService | null = null;
|
|
121
|
+
static #onContextChangeListeners: Array<(context: XCSHContext) => void> = [];
|
|
122
|
+
static #onAuthStatusChangeListeners: Array<(prev: AuthStatus, current: AuthStatus) => void> = [];
|
|
123
|
+
static #onTokenHealthChangeListeners: Array<(prev: TokenHealth, current: TokenHealth) => void> = [];
|
|
124
|
+
|
|
125
|
+
/** Register a callback invoked after a context is activated or its settings applied. */
|
|
126
|
+
static onContextChange(cb: (context: XCSHContext) => void): void {
|
|
127
|
+
ContextService.#onContextChangeListeners.push(cb);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Remove a previously-registered context-change callback. No-op if the callback isn't registered.
|
|
132
|
+
* Call on session disposal to prevent leaked listeners from mutating dead session state.
|
|
133
|
+
*/
|
|
134
|
+
static offContextChange(cb: (context: XCSHContext) => void): void {
|
|
135
|
+
const idx = ContextService.#onContextChangeListeners.indexOf(cb);
|
|
136
|
+
if (idx >= 0) ContextService.#onContextChangeListeners.splice(idx, 1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static onAuthStatusChange(cb: (prev: AuthStatus, current: AuthStatus) => void): void {
|
|
140
|
+
ContextService.#onAuthStatusChangeListeners.push(cb);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static offAuthStatusChange(cb: (prev: AuthStatus, current: AuthStatus) => void): void {
|
|
144
|
+
const idx = ContextService.#onAuthStatusChangeListeners.indexOf(cb);
|
|
145
|
+
if (idx >= 0) ContextService.#onAuthStatusChangeListeners.splice(idx, 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static onTokenHealthChange(cb: (prev: TokenHealth, current: TokenHealth) => void): void {
|
|
149
|
+
ContextService.#onTokenHealthChangeListeners.push(cb);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static offTokenHealthChange(cb: (prev: TokenHealth, current: TokenHealth) => void): void {
|
|
153
|
+
const idx = ContextService.#onTokenHealthChangeListeners.indexOf(cb);
|
|
154
|
+
if (idx >= 0) ContextService.#onTokenHealthChangeListeners.splice(idx, 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#configDir: string;
|
|
158
|
+
#activeContext: XCSHContext | null = null;
|
|
159
|
+
#credentialSource: ContextStatus["credentialSource"] = "none";
|
|
160
|
+
#authStatus: AuthStatus = "unknown";
|
|
161
|
+
#contextsCache: XCSHContext[] = [];
|
|
162
|
+
#namespacesCache: string[] = [];
|
|
163
|
+
/** Incremented on every `activate()`. Fire-and-forget namespace body-parses snapshot
|
|
164
|
+
* this at fetch time and discard the result if it has advanced — prevents a stale
|
|
165
|
+
* in-flight response from overwriting the cache after the active context changed. */
|
|
166
|
+
#activationEpoch = 0;
|
|
167
|
+
#lastAuthLatencyMs: number | undefined;
|
|
168
|
+
#lastAuthCheckedAt: number | undefined;
|
|
169
|
+
#apiClient: XCSHApiClient | null = null;
|
|
170
|
+
#cacheClient: XCSHApiClient | null = null;
|
|
171
|
+
#revalidationTimer: NodeJS.Timeout | null = null;
|
|
172
|
+
#lastTokenHealth: TokenHealth = "ok";
|
|
173
|
+
#previousContextName: string | null = null;
|
|
174
|
+
|
|
175
|
+
private constructor(configDir: string) {
|
|
176
|
+
this.#configDir = configDir;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#refreshApiClient(context: XCSHContext): void {
|
|
180
|
+
const apiUrl = context.apiUrl;
|
|
181
|
+
const apiToken = process.env[XCSH_API_TOKEN] ?? context.apiToken;
|
|
182
|
+
this.#apiClient = new XCSHApiClient({ apiUrl, apiToken });
|
|
183
|
+
// Best-effort background namespace-cache fill uses a non-retrying client.
|
|
184
|
+
// The cache is revalidated every 5 minutes (startRevalidation) and its
|
|
185
|
+
// errors are swallowed, so retries add no value here — and a multi-second
|
|
186
|
+
// backoff loop would float fire-and-forget well past the call that spawned
|
|
187
|
+
// it (in tests that share a global fetch mock, a late retry reads another
|
|
188
|
+
// test's mock and corrupts its count). One attempt keeps it bounded.
|
|
189
|
+
this.#cacheClient = new XCSHApiClient({ apiUrl, apiToken, maxRetries: 0 });
|
|
190
|
+
if (!hasEnvOverride()) {
|
|
191
|
+
this.#populateNamespaceCache();
|
|
192
|
+
}
|
|
193
|
+
this.startRevalidation();
|
|
194
|
+
this.#lastTokenHealth = "ok";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
getApiClient(): XCSHApiClient | null {
|
|
198
|
+
return this.#apiClient;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
startRevalidation(intervalMs = 300_000): void {
|
|
202
|
+
this.stopRevalidation();
|
|
203
|
+
const tick = async () => {
|
|
204
|
+
const previousStatus = this.#authStatus;
|
|
205
|
+
await this.validateToken();
|
|
206
|
+
if (this.#authStatus !== previousStatus) {
|
|
207
|
+
for (const cb of ContextService.#onAuthStatusChangeListeners) {
|
|
208
|
+
try {
|
|
209
|
+
cb(previousStatus, this.#authStatus);
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (this.#authStatus === "connected" && this.#namespacesCache.length === 0 && !hasEnvOverride()) {
|
|
214
|
+
this.#populateNamespaceCache();
|
|
215
|
+
}
|
|
216
|
+
const prevHealth = this.#lastTokenHealth;
|
|
217
|
+
const currentHealth = this.#computeTokenHealth();
|
|
218
|
+
if (currentHealth !== prevHealth) {
|
|
219
|
+
this.#lastTokenHealth = currentHealth;
|
|
220
|
+
for (const cb of ContextService.#onTokenHealthChangeListeners) {
|
|
221
|
+
try {
|
|
222
|
+
cb(prevHealth, currentHealth);
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (this.#revalidationTimer !== null) {
|
|
227
|
+
this.#revalidationTimer = setTimeout(tick, intervalMs);
|
|
228
|
+
this.#revalidationTimer.unref?.();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
this.#revalidationTimer = setTimeout(tick, intervalMs);
|
|
232
|
+
this.#revalidationTimer.unref?.();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
stopRevalidation(): void {
|
|
236
|
+
if (this.#revalidationTimer) {
|
|
237
|
+
clearTimeout(this.#revalidationTimer);
|
|
238
|
+
this.#revalidationTimer = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#computeTokenHealth(): TokenHealth {
|
|
243
|
+
const expiresAt = this.#activeContext?.metadata?.expiresAt;
|
|
244
|
+
if (!expiresAt) return "ok";
|
|
245
|
+
const diffMs = new Date(expiresAt).getTime() - Date.now();
|
|
246
|
+
if (diffMs <= 0) return "expired";
|
|
247
|
+
if (diffMs <= 7 * 86_400_000) return "expiring";
|
|
248
|
+
return "ok";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#populateNamespaceCache(): void {
|
|
252
|
+
const epochAtFetch = this.#activationEpoch;
|
|
253
|
+
const client = this.#cacheClient;
|
|
254
|
+
if (!client) return;
|
|
255
|
+
client
|
|
256
|
+
.listNamespaces()
|
|
257
|
+
.then(namespaces => {
|
|
258
|
+
if (this.#activationEpoch !== epochAtFetch) return;
|
|
259
|
+
this.#namespacesCache = namespaces.map(n => n.name).sort((a, b) => a.localeCompare(b));
|
|
260
|
+
})
|
|
261
|
+
.catch(err => {
|
|
262
|
+
logger.debug("XCSH namespace cache population failed", { error: String(err) });
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
static init(configDir: string): ContextService {
|
|
267
|
+
ContextService.#instance = new ContextService(configDir);
|
|
268
|
+
return ContextService.#instance;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Return the existing instance, or bootstrap one using the provided
|
|
273
|
+
* configDir (or `getXCSHConfigDir()` if omitted) and run loadActive()
|
|
274
|
+
* before returning.
|
|
275
|
+
*
|
|
276
|
+
* Primary patterns used in-tree:
|
|
277
|
+
* - main.ts: CLI startup calls `ContextService.init(dir).loadActive()`
|
|
278
|
+
* eagerly — deterministic, synchronous path for the CLI.
|
|
279
|
+
* - SDK/embedder paths and slash-command handlers call
|
|
280
|
+
* `ContextService.getOrInit()` — returns existing or bootstraps.
|
|
281
|
+
* - Synchronous render paths (e.g. status-line segments) call `.instance`
|
|
282
|
+
* inside try/catch and silently hide if uninitialized — they MUST NOT
|
|
283
|
+
* trigger bootstrapping as a side effect of rendering.
|
|
284
|
+
* - welcome-checks.ts reads `.instance` directly after startup has already
|
|
285
|
+
* populated the singleton via init().
|
|
286
|
+
*
|
|
287
|
+
* No race exists: `init()` is synchronous, so a concurrent caller always
|
|
288
|
+
* observes the populated singleton before re-entering the null branch.
|
|
289
|
+
*
|
|
290
|
+
* @param configDir — seed directory when bootstrapping; ignored when an
|
|
291
|
+
* instance already exists.
|
|
292
|
+
* @param cwd — working directory for local context resolution; passed to
|
|
293
|
+
* loadActive(). Ignored when an instance already exists.
|
|
294
|
+
*/
|
|
295
|
+
static async getOrInit(configDir?: string, cwd?: string): Promise<ContextService> {
|
|
296
|
+
if (ContextService.#instance) return ContextService.#instance;
|
|
297
|
+
const dir = configDir ?? getXCSHConfigDir();
|
|
298
|
+
const service = ContextService.init(dir);
|
|
299
|
+
await service.loadActive(cwd);
|
|
300
|
+
return service;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Return the values of env vars marked as sensitive in the active context.
|
|
305
|
+
* Safe to call before init — returns empty array if no context is loaded.
|
|
306
|
+
*/
|
|
307
|
+
static getSensitiveContextValues(): string[] {
|
|
308
|
+
const instance = ContextService.#instance;
|
|
309
|
+
if (!instance) return [];
|
|
310
|
+
const context = instance.#activeContext;
|
|
311
|
+
if (!context?.sensitiveKeys?.length || !context.env) return [];
|
|
312
|
+
const values: string[] = [];
|
|
313
|
+
for (const key of context.sensitiveKeys) {
|
|
314
|
+
const value = context.env[key];
|
|
315
|
+
if (value) values.push(value);
|
|
316
|
+
}
|
|
317
|
+
return values;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static get instance(): ContextService {
|
|
321
|
+
if (!ContextService.#instance) {
|
|
322
|
+
throw new Error("ContextService not initialized. Call ContextService.init() first.");
|
|
323
|
+
}
|
|
324
|
+
return ContextService.#instance;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static _resetForTest(): void {
|
|
328
|
+
if (ContextService.#instance) {
|
|
329
|
+
ContextService.#instance.#apiClient = null;
|
|
330
|
+
ContextService.#instance.#cacheClient = null;
|
|
331
|
+
ContextService.#instance.#lastTokenHealth = "ok";
|
|
332
|
+
ContextService.#instance.stopRevalidation();
|
|
333
|
+
ContextService.#instance.#previousContextName = null;
|
|
334
|
+
}
|
|
335
|
+
ContextService.#instance = null;
|
|
336
|
+
// Clear listeners to prevent cross-test contamination. Each createAgentSession() call
|
|
337
|
+
// registers a listener closed over that session's sessionManager; without this reset,
|
|
338
|
+
// listeners from a disposed session persist into the next test and fire on activate().
|
|
339
|
+
ContextService.#onContextChangeListeners = [];
|
|
340
|
+
ContextService.#onAuthStatusChangeListeners = [];
|
|
341
|
+
ContextService.#onTokenHealthChangeListeners = [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
get previousContextName(): string | null {
|
|
345
|
+
return this.#previousContextName;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
get contextsDir(): string {
|
|
349
|
+
return path.join(this.#configDir, "contexts");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
get activeContextPath(): string {
|
|
353
|
+
return path.join(this.#configDir, "active_context");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Active context's API/console base URL, or null when no context is active. */
|
|
357
|
+
get activeApiUrl(): string | null {
|
|
358
|
+
return this.#activeContext?.apiUrl ?? null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Active context's default namespace, or null when no context is active. */
|
|
362
|
+
get activeNamespace(): string | null {
|
|
363
|
+
return this.#activeContext?.defaultNamespace ?? null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async loadActive(cwd?: string): Promise<XCSHContext | null> {
|
|
367
|
+
// FR-102: XCSH_API_URL is the signal to skip context loading entirely.
|
|
368
|
+
// Subprocesses inherit process.env, so they already see the env vars directly.
|
|
369
|
+
if (process.env[XCSH_API_URL]) {
|
|
370
|
+
this.#credentialSource = "environment";
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// FR-201: Check for project-local context in .xcsh/contexts/
|
|
375
|
+
if (cwd) {
|
|
376
|
+
const resolver = new ContextResolver({ paths: xcshContextPaths });
|
|
377
|
+
const localResult = await resolver.resolve(cwd);
|
|
378
|
+
if (localResult && localResult.source === "local") {
|
|
379
|
+
const localContext = localResult.context as XCSHContext;
|
|
380
|
+
// Gate: incompatible schema version
|
|
381
|
+
let versionOk = true;
|
|
382
|
+
try {
|
|
383
|
+
this.#assertCompatibleVersion(localContext);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
versionOk = false;
|
|
386
|
+
logger.warn("XCSH: local context uses incompatible schema version, skipping", {
|
|
387
|
+
sourcePath: localResult.sourcePath,
|
|
388
|
+
error: String(err),
|
|
389
|
+
});
|
|
390
|
+
// Fall through to global resolution below
|
|
391
|
+
}
|
|
392
|
+
if (versionOk) {
|
|
393
|
+
this.#activeContext = localContext;
|
|
394
|
+
this.#applyToSettings(localContext);
|
|
395
|
+
this.#credentialSource = hasEnvOverride() ? "mixed" : "context";
|
|
396
|
+
this.#refreshApiClient(localContext);
|
|
397
|
+
logger.debug("XCSH: using project context", {
|
|
398
|
+
name: localContext.name,
|
|
399
|
+
source: localResult.sourcePath,
|
|
400
|
+
});
|
|
401
|
+
// Run git-tracking safety check asynchronously
|
|
402
|
+
resolver
|
|
403
|
+
.checkGitTracking(localResult.sourcePath)
|
|
404
|
+
.then(tracked => {
|
|
405
|
+
if (tracked) {
|
|
406
|
+
logger.warn(
|
|
407
|
+
`WARNING: ${localResult.sourcePath} is tracked by git! ` +
|
|
408
|
+
`This file may contain credentials. Run: git rm --cached ${localResult.sourcePath}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
.catch(() => {});
|
|
413
|
+
return localContext;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check if config dir exists
|
|
419
|
+
if (!fs.existsSync(this.#configDir)) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Seed the context cache so `/context activate <tab>` has data at startup.
|
|
424
|
+
// listContexts is declared async but its body uses fs.readdirSync /
|
|
425
|
+
// readFileSync — the cost is proportional to the number of context files.
|
|
426
|
+
// For typical N ≤ 10 on local disk this is sub-millisecond; contexts are
|
|
427
|
+
// small JSON files. A future refactor to fs.promises + truly async I/O
|
|
428
|
+
// would let startup proceed in parallel with the reads, but the current
|
|
429
|
+
// sync form keeps createContext/deleteContext race-free with no coordination.
|
|
430
|
+
await this.listContexts();
|
|
431
|
+
|
|
432
|
+
let contextName = this.#readActiveContextName();
|
|
433
|
+
|
|
434
|
+
// FR-104: auto-activate if exactly one context exists
|
|
435
|
+
let autoActivated = false;
|
|
436
|
+
if (!contextName) {
|
|
437
|
+
const contexts = this.#listContextFiles();
|
|
438
|
+
if (contexts.length === 1) {
|
|
439
|
+
contextName = contexts[0].replace(/\.json$/, "");
|
|
440
|
+
autoActivated = true;
|
|
441
|
+
} else {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Read the context JSON
|
|
447
|
+
const context = this.#readContext(contextName);
|
|
448
|
+
if (!context) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Gate: incompatible schema version — log warning and return null (don't crash startup)
|
|
453
|
+
try {
|
|
454
|
+
this.#assertCompatibleVersion(context);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
logger.warn("XCSH: context uses incompatible schema version, skipping", {
|
|
457
|
+
name: contextName,
|
|
458
|
+
error: String(err),
|
|
459
|
+
});
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Only persist active_context after the context validates
|
|
464
|
+
if (autoActivated) {
|
|
465
|
+
this.#atomicWrite(this.activeContextPath, contextName);
|
|
466
|
+
logger.debug("XCSH: auto-activated single context", { name: contextName });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.#activeContext = context;
|
|
470
|
+
this.#applyToSettings(context);
|
|
471
|
+
// Detect mixed source: context loaded but some fields come from process.env
|
|
472
|
+
this.#credentialSource = hasEnvOverride() ? "mixed" : "context";
|
|
473
|
+
this.#refreshApiClient(context);
|
|
474
|
+
return context;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async activate(name: string): Promise<XCSHContext> {
|
|
478
|
+
// Reject activation when env overrides are present — before any I/O
|
|
479
|
+
if (process.env[XCSH_API_URL]) {
|
|
480
|
+
throw new ContextError(
|
|
481
|
+
"Cannot activate: XCSH_API_URL environment variable overrides context. Run `unset XCSH_API_URL` first, or restart without it.",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Self-heal: activate called before loadActive ever ran. Populate cache.
|
|
486
|
+
if (this.#contextsCache.length === 0) {
|
|
487
|
+
await this.listContexts();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.#validateContextName(name);
|
|
491
|
+
const context = this.#readContext(name);
|
|
492
|
+
if (!context) {
|
|
493
|
+
throw new ContextError(`Context '${name}' not found. Run \`/context list\` to see available contexts.`, name);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
this.#assertCompatibleVersion(context);
|
|
497
|
+
|
|
498
|
+
// NFR-402: write active_context first — if it fails, don't update settings
|
|
499
|
+
this.#atomicWrite(this.activeContextPath, name);
|
|
500
|
+
|
|
501
|
+
if (this.#activeContext && this.#activeContext.name !== name) {
|
|
502
|
+
this.#previousContextName = this.#activeContext.name;
|
|
503
|
+
}
|
|
504
|
+
this.#activeContext = context;
|
|
505
|
+
this.#applyToSettings(context);
|
|
506
|
+
this.#credentialSource = hasEnvOverride() ? "mixed" : "context";
|
|
507
|
+
this.#namespacesCache = [];
|
|
508
|
+
this.#activationEpoch += 1;
|
|
509
|
+
|
|
510
|
+
// Invalidate auth-freshness cache on context switch — the previous context's latency
|
|
511
|
+
// and "checked N min ago" timestamp are stale now that a different tenant is active.
|
|
512
|
+
// Subsequent validateToken() (e.g., from /context status) repopulates these fields.
|
|
513
|
+
this.#authStatus = "unknown";
|
|
514
|
+
this.#lastAuthLatencyMs = undefined;
|
|
515
|
+
this.#lastAuthCheckedAt = undefined;
|
|
516
|
+
this.#refreshApiClient(context);
|
|
517
|
+
|
|
518
|
+
return context;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async activatePrevious(): Promise<XCSHContext> {
|
|
522
|
+
if (!this.#previousContextName) {
|
|
523
|
+
throw new ContextError("No previous context. Switch contexts first with /context activate <name>.");
|
|
524
|
+
}
|
|
525
|
+
return this.activate(this.#previousContextName);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async listContexts(): Promise<XCSHContext[]> {
|
|
529
|
+
const files = this.#listContextFiles();
|
|
530
|
+
const contexts: XCSHContext[] = [];
|
|
531
|
+
for (const file of files) {
|
|
532
|
+
const name = file.replace(/\.json$/, "");
|
|
533
|
+
// Skip files whose basename doesn't satisfy the context-name contract —
|
|
534
|
+
// they cannot be activated (#validateContextName would reject), so
|
|
535
|
+
// surfacing them in /context list or /context activate <tab> just
|
|
536
|
+
// offers users a selection that the handler will immediately refuse.
|
|
537
|
+
if (!this.#isValidContextName(name)) {
|
|
538
|
+
logger.warn("XCSH context file has invalid name, skipping", { name });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const context = this.#readContext(name);
|
|
542
|
+
if (context) {
|
|
543
|
+
contexts.push(context);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
this.#contextsCache = [...contexts].sort((a, b) => a.name.localeCompare(b.name));
|
|
547
|
+
return [...this.#contextsCache];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async createContext(context: Omit<XCSHContext, "metadata" | "version">): Promise<void> {
|
|
551
|
+
this.#validateContextName(context.name);
|
|
552
|
+
this.#assertNotReserved(context.name);
|
|
553
|
+
const contextPath = path.join(this.contextsDir, `${context.name}.json`);
|
|
554
|
+
if (fs.existsSync(contextPath)) {
|
|
555
|
+
throw new ContextError(`Context '${context.name}' already exists.`, context.name);
|
|
556
|
+
}
|
|
557
|
+
fs.mkdirSync(this.contextsDir, { recursive: true, mode: 0o700 });
|
|
558
|
+
fs.mkdirSync(this.#configDir, { recursive: true, mode: 0o700 });
|
|
559
|
+
const data: XCSHContext = {
|
|
560
|
+
...context,
|
|
561
|
+
// Store the endpoint as origin only; callers append `/api/...` paths.
|
|
562
|
+
apiUrl: normalizeApiUrl(context.apiUrl),
|
|
563
|
+
version: CURRENT_SCHEMA_VERSION,
|
|
564
|
+
metadata: { createdAt: new Date().toISOString() },
|
|
565
|
+
};
|
|
566
|
+
const filePayload = {
|
|
567
|
+
$schema:
|
|
568
|
+
"https://raw.githubusercontent.com/f5xc-salesdemos/xcsh/main/packages/coding-agent/src/config/context-schema.json",
|
|
569
|
+
...data,
|
|
570
|
+
} as Record<string, unknown>;
|
|
571
|
+
const tmpPath = `${contextPath}.tmp`;
|
|
572
|
+
fs.writeFileSync(tmpPath, JSON.stringify(filePayload, null, 2), { mode: 0o600 });
|
|
573
|
+
fs.renameSync(tmpPath, contextPath);
|
|
574
|
+
this.#contextsCache = [...this.#contextsCache, data].sort((a, b) => a.name.localeCompare(b.name));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async deleteContext(name: string): Promise<void> {
|
|
578
|
+
this.#validateContextName(name);
|
|
579
|
+
const contextPath = path.join(this.contextsDir, `${name}.json`);
|
|
580
|
+
if (!fs.existsSync(contextPath)) {
|
|
581
|
+
throw new ContextError(`Context '${name}' not found.`, name);
|
|
582
|
+
}
|
|
583
|
+
fs.unlinkSync(contextPath);
|
|
584
|
+
this.#contextsCache = this.#contextsCache.filter(p => p.name !== name);
|
|
585
|
+
if (this.#previousContextName === name) {
|
|
586
|
+
this.#previousContextName = null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Export one or more contexts as an ExportBundle. Contexts are deep-cloned
|
|
592
|
+
* before any masking to guarantee the in-memory cache (#contextsCache and
|
|
593
|
+
* #activeContext, which may share references) is never mutated.
|
|
594
|
+
*
|
|
595
|
+
* When includeToken is false, apiToken and every env value whose key is in
|
|
596
|
+
* sensitiveKeys is replaced with the masked form. The envelope's
|
|
597
|
+
* tokensMasked flag reflects this so importContexts can refuse masked
|
|
598
|
+
* bundles.
|
|
599
|
+
*
|
|
600
|
+
* Throws ContextError when a requested name does not exist on disk.
|
|
601
|
+
*/
|
|
602
|
+
async exportContexts(opts: { names?: string[]; includeToken: boolean }): Promise<ExportBundle> {
|
|
603
|
+
const all = await this.listContexts();
|
|
604
|
+
let selected: XCSHContext[];
|
|
605
|
+
if (opts.names && opts.names.length > 0) {
|
|
606
|
+
const byName = new Map(all.map(p => [p.name, p]));
|
|
607
|
+
selected = [];
|
|
608
|
+
const missing: string[] = [];
|
|
609
|
+
for (const n of opts.names) {
|
|
610
|
+
const p = byName.get(n);
|
|
611
|
+
if (!p) missing.push(n);
|
|
612
|
+
else selected.push(p);
|
|
613
|
+
}
|
|
614
|
+
if (missing.length > 0) {
|
|
615
|
+
throw new ContextError(`Context(s) not found: ${missing.join(", ")}.`, missing[0]);
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
selected = all;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Deep-clone BEFORE masking. maskToken is destructive; mutating cache
|
|
622
|
+
// entries would break subsequent activate/validate/show operations.
|
|
623
|
+
const cloned = selected.map(p => structuredClone(p));
|
|
624
|
+
|
|
625
|
+
if (!opts.includeToken) {
|
|
626
|
+
for (const p of cloned) {
|
|
627
|
+
p.apiToken = this.maskToken(p.apiToken);
|
|
628
|
+
if (p.env) {
|
|
629
|
+
// Mask env values whose key is either in sensitiveKeys OR
|
|
630
|
+
// matches SECRET_ENV_PATTERNS. Mirrors the show() handler's
|
|
631
|
+
// masking contract: `setEnvVars` auto-populates sensitiveKeys
|
|
632
|
+
// from the pattern, but contexts edited directly on disk or
|
|
633
|
+
// imported from older formats may have secret-looking keys
|
|
634
|
+
// (e.g. XCSH_CONSOLE_PASSWORD, *_TOKEN, *_SECRET) without
|
|
635
|
+
// `sensitiveKeys` entries. Export must match show() to avoid
|
|
636
|
+
// leaking credentials that show() already masks.
|
|
637
|
+
const sensitive = new Set(p.sensitiveKeys ?? []);
|
|
638
|
+
for (const [k, v] of Object.entries(p.env)) {
|
|
639
|
+
if (sensitive.has(k) || SECRET_ENV_PATTERNS.test(k)) {
|
|
640
|
+
p.env[k] = this.maskToken(v);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
version: CURRENT_EXPORT_VERSION,
|
|
649
|
+
exportedAt: new Date().toISOString(),
|
|
650
|
+
tokensMasked: !opts.includeToken,
|
|
651
|
+
contexts: cloned,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Import contexts from a bundle. Validation order is load-bearing:
|
|
657
|
+
* 1. Envelope schema (object with version/tokensMasked/contexts).
|
|
658
|
+
* 2. Version match.
|
|
659
|
+
* 3. tokensMasked: true is rejected — masked tokens would pass write but
|
|
660
|
+
* fail runtime auth with a misleading error.
|
|
661
|
+
* 4. Per-context field-shape via #validateContextShape — any failure
|
|
662
|
+
* rejects the whole import; no writes occur.
|
|
663
|
+
* 5. Conflict detection against a fresh listContexts() read — not the
|
|
664
|
+
* in-memory cache, which can miss concurrent-session edits.
|
|
665
|
+
* 6. Atomic per-file write loop. Each write is atomic individually via
|
|
666
|
+
* #atomicWrite, but the overall import is NOT transactional: if the
|
|
667
|
+
* Nth of M writes throws, the first N-1 contexts are kept and the
|
|
668
|
+
* remainder are not written. Multi-file rollback would require a
|
|
669
|
+
* two-phase commit we do not implement; validation steps 1–5 catch
|
|
670
|
+
* all foreseeable failures before any write begins.
|
|
671
|
+
* 7. Cache refresh.
|
|
672
|
+
*/
|
|
673
|
+
async importContexts(bundle: unknown, opts: { overwrite: boolean }): Promise<ImportResult> {
|
|
674
|
+
// 1. Envelope schema
|
|
675
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
676
|
+
throw new ContextError("Import bundle missing required fields: bundle must be an object.");
|
|
677
|
+
}
|
|
678
|
+
const b = bundle as Record<string, unknown>;
|
|
679
|
+
if (typeof b.version !== "number" || typeof b.tokensMasked !== "boolean" || !Array.isArray(b.contexts)) {
|
|
680
|
+
throw new ContextError(
|
|
681
|
+
"Import bundle missing required fields: expected { version: number, tokensMasked: boolean, contexts: array }.",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 2. Version
|
|
686
|
+
if (b.version !== CURRENT_EXPORT_VERSION) {
|
|
687
|
+
throw new ContextError(
|
|
688
|
+
`Import bundle uses export version ${b.version}, but this version of xcsh only supports ${CURRENT_EXPORT_VERSION}.`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 3. Masked-token gate
|
|
693
|
+
if (b.tokensMasked === true) {
|
|
694
|
+
throw new ContextError(
|
|
695
|
+
"Bundle contains masked tokens. Re-export with --include-token to produce an importable bundle.",
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// 4. Per-context field-shape
|
|
700
|
+
const rawContexts = b.contexts as unknown[];
|
|
701
|
+
const normalized: XCSHContext[] = [];
|
|
702
|
+
const badNames: string[] = [];
|
|
703
|
+
for (let i = 0; i < rawContexts.length; i++) {
|
|
704
|
+
const raw = rawContexts[i];
|
|
705
|
+
const rawObj = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
|
706
|
+
const name = typeof rawObj.name === "string" ? rawObj.name : `<entry ${i}>`;
|
|
707
|
+
if (typeof rawObj.name !== "string" || !this.#isValidContextName(rawObj.name)) {
|
|
708
|
+
badNames.push(`${name} (invalid name)`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (RESERVED_CONTEXT_NAMES.has(rawObj.name.toLowerCase())) {
|
|
712
|
+
badNames.push(`${rawObj.name} (reserved subcommand name)`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const shape = this.#validateContextShape(raw, rawObj.name);
|
|
716
|
+
if (!shape) {
|
|
717
|
+
badNames.push(`${rawObj.name} (invalid shape)`);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
normalized.push(shape);
|
|
721
|
+
}
|
|
722
|
+
if (badNames.length > 0) {
|
|
723
|
+
throw new ContextError(`Import bundle has ${badNames.length} invalid context(s): ${badNames.join(", ")}.`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 4.5. Per-context schema-version compatibility. The envelope version
|
|
727
|
+
// (step 2) is the bundle format; `context.version` is the per-context
|
|
728
|
+
// schema version. Without this check a bundle produced by a newer xcsh
|
|
729
|
+
// (version: 2) would pass shape checks and reach the write loop,
|
|
730
|
+
// leaving unusable contexts on disk that activate/loadActive reject.
|
|
731
|
+
// In the overwrite-active path that would mean the active context is
|
|
732
|
+
// silently bricked on the next startup. Reject upfront.
|
|
733
|
+
const incompatibleNames: string[] = [];
|
|
734
|
+
for (const p of normalized) {
|
|
735
|
+
if (p.version !== undefined && p.version > CURRENT_SCHEMA_VERSION) {
|
|
736
|
+
incompatibleNames.push(`${p.name} (v${p.version})`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (incompatibleNames.length > 0) {
|
|
740
|
+
throw new ContextError(
|
|
741
|
+
`Import bundle has ${incompatibleNames.length} context(s) with incompatible schema version (this xcsh supports v${CURRENT_SCHEMA_VERSION}): ${incompatibleNames.join(", ")}. Upgrade xcsh to import this bundle.`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// 4.6. Intra-bundle duplicate-name rejection. A bundle listing the
|
|
746
|
+
// same name twice would silently clobber the first entry in the write
|
|
747
|
+
// loop and emit misleading duplicated names in `imported[]`. Reject
|
|
748
|
+
// before any write so the user can fix the malformed bundle.
|
|
749
|
+
const seen = new Set<string>();
|
|
750
|
+
const intraDuplicates = new Set<string>();
|
|
751
|
+
for (const p of normalized) {
|
|
752
|
+
if (seen.has(p.name)) intraDuplicates.add(p.name);
|
|
753
|
+
else seen.add(p.name);
|
|
754
|
+
}
|
|
755
|
+
if (intraDuplicates.size > 0) {
|
|
756
|
+
throw new ContextError(
|
|
757
|
+
`Import bundle contains duplicate context name(s): ${[...intraDuplicates].join(", ")}. Each name must appear at most once.`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// 5. Conflict detection — fresh disk read, NOT listContextNamesCached
|
|
762
|
+
const existing = await this.listContexts();
|
|
763
|
+
const existingNames = new Set(existing.map(p => p.name));
|
|
764
|
+
const conflicts = normalized.filter(p => existingNames.has(p.name)).map(p => p.name);
|
|
765
|
+
if (conflicts.length > 0 && !opts.overwrite) {
|
|
766
|
+
throw new ContextError(
|
|
767
|
+
`${conflicts.length} context(s) conflict: ${conflicts.join(", ")}. Re-run with --overwrite to replace, or delete conflicts first.`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// 6. Write loop — atomic per-file
|
|
772
|
+
fs.mkdirSync(this.contextsDir, { recursive: true, mode: 0o700 });
|
|
773
|
+
const imported: string[] = [];
|
|
774
|
+
const overwritten: string[] = [];
|
|
775
|
+
for (const context of normalized) {
|
|
776
|
+
const filePath = path.join(this.contextsDir, `${context.name}.json`);
|
|
777
|
+
const wasExisting = existingNames.has(context.name);
|
|
778
|
+
const payload: XCSHContext = {
|
|
779
|
+
...context,
|
|
780
|
+
version: context.version ?? CURRENT_SCHEMA_VERSION,
|
|
781
|
+
metadata: context.metadata ?? { createdAt: new Date().toISOString() },
|
|
782
|
+
};
|
|
783
|
+
this.#atomicWrite(filePath, JSON.stringify(payload, null, 2));
|
|
784
|
+
imported.push(context.name);
|
|
785
|
+
if (wasExisting) overwritten.push(context.name);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 7. Cache refresh
|
|
789
|
+
await this.listContexts();
|
|
790
|
+
|
|
791
|
+
// 8. Refresh active-context state if its backing file was overwritten.
|
|
792
|
+
// importContexts's write loop replaces the on-disk JSON, but #activeContext,
|
|
793
|
+
// Settings.bash.environment (apiUrl/apiToken/namespace), and the cached
|
|
794
|
+
// auth metadata all hold a snapshot from the prior activate() call. Without
|
|
795
|
+
// this step, a successful `/context import --overwrite` that touches the
|
|
796
|
+
// active context leaves the session talking to the wrong tenant with the
|
|
797
|
+
// wrong token until the user restarts or re-activates manually.
|
|
798
|
+
const activeName = this.#activeContext?.name;
|
|
799
|
+
if (activeName && overwritten.includes(activeName)) {
|
|
800
|
+
await this.activate(activeName);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return { imported, overwritten, skipped: [] };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Rename a context. File is renamed first (atomic rename(2)); if the context
|
|
808
|
+
* is active, active_context is then updated to point at the new name. If the
|
|
809
|
+
* pointer update fails, the file rename is rolled back.
|
|
810
|
+
*
|
|
811
|
+
* Throws ContextError for invalid names, missing source, or a target name
|
|
812
|
+
* that already exists. If the pointer-write rollback itself fails, logs a
|
|
813
|
+
* warning and throws a ContextError documenting the inconsistent filesystem
|
|
814
|
+
* state for manual recovery.
|
|
815
|
+
*
|
|
816
|
+
* Fires onContextChange listeners when the active context is renamed.
|
|
817
|
+
*
|
|
818
|
+
* Note: does not rewrite the JSON body's "name" field. #readContext treats
|
|
819
|
+
* the filename as canonical identity, so the stale field is inert.
|
|
820
|
+
*/
|
|
821
|
+
async renameContext(oldName: string, newName: string): Promise<void> {
|
|
822
|
+
this.#validateContextName(oldName);
|
|
823
|
+
this.#validateContextName(newName);
|
|
824
|
+
this.#assertNotReserved(newName);
|
|
825
|
+
|
|
826
|
+
const oldPath = path.join(this.contextsDir, `${oldName}.json`);
|
|
827
|
+
const newPath = path.join(this.contextsDir, `${newName}.json`);
|
|
828
|
+
|
|
829
|
+
// Existence check fires BEFORE the identity short-circuit so
|
|
830
|
+
// `renameContext("ghost", "ghost")` returns the expected not-found error
|
|
831
|
+
// instead of a silent success that hides a typo.
|
|
832
|
+
if (!fs.existsSync(oldPath)) {
|
|
833
|
+
throw new ContextError(`Context '${oldName}' not found.`, oldName);
|
|
834
|
+
}
|
|
835
|
+
if (oldName === newName) return;
|
|
836
|
+
if (fs.existsSync(newPath)) {
|
|
837
|
+
throw new ContextError(`Context '${newName}' already exists.`, newName);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Step 1: rename file (atomic rename(2) on the same filesystem)
|
|
841
|
+
fs.renameSync(oldPath, newPath);
|
|
842
|
+
|
|
843
|
+
// Step 2: if renaming the active context, update the pointer. On failure
|
|
844
|
+
// we must roll back the file rename so the user sees a consistent state.
|
|
845
|
+
// Consult BOTH the hydrated in-memory state AND the on-disk pointer:
|
|
846
|
+
// loadActive() leaves #activeContext null when XCSH_API_URL overrides
|
|
847
|
+
// the context, but the on-disk active_context file may still name the
|
|
848
|
+
// context being renamed — and the next non-env session relies on that
|
|
849
|
+
// pointer to restore the user's active selection.
|
|
850
|
+
const onDiskActiveName = this.#readActiveContextName();
|
|
851
|
+
const wasActive = this.#activeContext?.name === oldName || onDiskActiveName === oldName;
|
|
852
|
+
if (wasActive) {
|
|
853
|
+
try {
|
|
854
|
+
this.#atomicWrite(this.activeContextPath, newName);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
// Rollback. Inner try wraps ONLY the rename-back call so the
|
|
857
|
+
// rollback-succeeded / rollback-failed paths are clearly separated.
|
|
858
|
+
try {
|
|
859
|
+
fs.renameSync(newPath, oldPath);
|
|
860
|
+
} catch (rollbackErr) {
|
|
861
|
+
logger.warn("XCSH context rename rollback failed — manual recovery required", {
|
|
862
|
+
oldName,
|
|
863
|
+
newName,
|
|
864
|
+
originalError: String(err),
|
|
865
|
+
rollbackError: String(rollbackErr),
|
|
866
|
+
});
|
|
867
|
+
throw new ContextError(
|
|
868
|
+
`Rename failed and rollback failed. Filesystem state: contexts/${newName}.json exists, active_context still points at '${oldName}'. Manually rename contexts/${newName}.json back to contexts/${oldName}.json, or update active_context to '${newName}'. Original error: ${err instanceof Error ? err.message : String(err)}. Rollback error: ${String(rollbackErr)}`,
|
|
869
|
+
oldName,
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
// Rollback succeeded — throw the user-friendly error.
|
|
873
|
+
throw new ContextError(
|
|
874
|
+
`Failed to update active context pointer: ${err instanceof Error ? err.message : String(err)}. Context was not renamed.`,
|
|
875
|
+
oldName,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Step 3: update cache + active-context pointer in memory.
|
|
881
|
+
// Private-static listener access uses the same idiom as #applyToSettings
|
|
882
|
+
// (the loop `for (const cb of ContextService.#onContextChangeListeners)`
|
|
883
|
+
// already appears in that method) — direct `ContextService.#name` access
|
|
884
|
+
// from inside the class body.
|
|
885
|
+
this.#contextsCache = this.#contextsCache
|
|
886
|
+
.map(p => (p.name === oldName ? { ...p, name: newName } : p))
|
|
887
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
888
|
+
if (this.#previousContextName === oldName) {
|
|
889
|
+
this.#previousContextName = newName;
|
|
890
|
+
}
|
|
891
|
+
if (wasActive && this.#activeContext) {
|
|
892
|
+
this.#activeContext = { ...this.#activeContext, name: newName };
|
|
893
|
+
for (const cb of ContextService.#onContextChangeListeners) {
|
|
894
|
+
cb(this.#activeContext);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** Add or update environment variables on a context. Keys matching secret
|
|
900
|
+
* naming patterns are automatically added to sensitiveKeys. */
|
|
901
|
+
async setEnvVars(name: string, vars: Record<string, string>): Promise<{ sensitive: string[] }> {
|
|
902
|
+
this.#validateContextName(name);
|
|
903
|
+
const context = this.#readContext(name);
|
|
904
|
+
if (!context) throw new ContextError(`Context '${name}' not found.`, name);
|
|
905
|
+
|
|
906
|
+
this.#assertCompatibleVersion(context);
|
|
907
|
+
|
|
908
|
+
const reservedViolations = Object.keys(vars).filter(k => RESERVED_ENV_KEYS.has(k));
|
|
909
|
+
if (reservedViolations.length > 0) {
|
|
910
|
+
const messages = reservedViolations.map(k => RESERVED_ENV_MESSAGES[k]).join("\n");
|
|
911
|
+
throw new ContextError(messages, name);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const env = { ...(context.env ?? {}), ...vars };
|
|
915
|
+
const sensitiveSet = new Set(context.sensitiveKeys ?? []);
|
|
916
|
+
const newSensitive: string[] = [];
|
|
917
|
+
for (const key of Object.keys(vars)) {
|
|
918
|
+
if (SECRET_ENV_PATTERNS.test(key) && !sensitiveSet.has(key)) {
|
|
919
|
+
sensitiveSet.add(key);
|
|
920
|
+
newSensitive.push(key);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
// Remove sensitiveKeys entries for keys no longer in env
|
|
924
|
+
const sensitiveKeys = [...sensitiveSet].filter(k => k in env);
|
|
925
|
+
|
|
926
|
+
const updated: XCSHContext = {
|
|
927
|
+
...context,
|
|
928
|
+
env,
|
|
929
|
+
sensitiveKeys: sensitiveKeys.length > 0 ? sensitiveKeys : undefined,
|
|
930
|
+
};
|
|
931
|
+
const contextPath = path.join(this.contextsDir, `${name}.json`);
|
|
932
|
+
this.#atomicWrite(contextPath, JSON.stringify(updated, null, 2));
|
|
933
|
+
this.#contextsCache = this.#contextsCache.map(p => (p.name === name ? updated : p));
|
|
934
|
+
|
|
935
|
+
if (this.#activeContext?.name === name) {
|
|
936
|
+
this.#activeContext = updated;
|
|
937
|
+
this.#applyToSettings(updated);
|
|
938
|
+
}
|
|
939
|
+
return { sensitive: newSensitive };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Remove environment variables from a context. Also removes them from sensitiveKeys. */
|
|
943
|
+
async unsetEnvVars(name: string, keys: string[]): Promise<{ removed: string[] }> {
|
|
944
|
+
this.#validateContextName(name);
|
|
945
|
+
const context = this.#readContext(name);
|
|
946
|
+
if (!context) throw new ContextError(`Context '${name}' not found.`, name);
|
|
947
|
+
|
|
948
|
+
this.#assertCompatibleVersion(context);
|
|
949
|
+
|
|
950
|
+
const env = { ...(context.env ?? {}) };
|
|
951
|
+
const removed: string[] = [];
|
|
952
|
+
for (const key of keys) {
|
|
953
|
+
if (key in env) {
|
|
954
|
+
delete env[key];
|
|
955
|
+
removed.push(key);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (removed.length === 0) return { removed: [] };
|
|
959
|
+
|
|
960
|
+
const keySet = new Set(keys);
|
|
961
|
+
const sensitiveKeys = (context.sensitiveKeys ?? []).filter(k => !keySet.has(k) && k in env);
|
|
962
|
+
const envOrUndefined = Object.keys(env).length > 0 ? env : undefined;
|
|
963
|
+
|
|
964
|
+
const updated: XCSHContext = {
|
|
965
|
+
...context,
|
|
966
|
+
env: envOrUndefined,
|
|
967
|
+
sensitiveKeys: sensitiveKeys.length > 0 ? sensitiveKeys : undefined,
|
|
968
|
+
};
|
|
969
|
+
const contextPath = path.join(this.contextsDir, `${name}.json`);
|
|
970
|
+
this.#atomicWrite(contextPath, JSON.stringify(updated, null, 2));
|
|
971
|
+
this.#contextsCache = this.#contextsCache.map(p => (p.name === name ? updated : p));
|
|
972
|
+
|
|
973
|
+
if (this.#activeContext?.name === name) {
|
|
974
|
+
this.#activeContext = updated;
|
|
975
|
+
this.#applyToSettings(updated);
|
|
976
|
+
}
|
|
977
|
+
return { removed };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async validateToken(options?: {
|
|
981
|
+
timeoutMs?: number;
|
|
982
|
+
apiUrl?: string;
|
|
983
|
+
apiToken?: string;
|
|
984
|
+
}): Promise<{ status: AuthStatus; latencyMs?: number; errorClass?: "network" | "credential" | "url_not_found" }> {
|
|
985
|
+
// Use explicit credentials if provided (for non-active contexts or env-backed sessions),
|
|
986
|
+
// otherwise fall back to effective credentials (env override > active context)
|
|
987
|
+
const effectiveUrl = options?.apiUrl ?? process.env[XCSH_API_URL] ?? this.#activeContext?.apiUrl;
|
|
988
|
+
const effectiveToken = options?.apiToken ?? process.env[XCSH_API_TOKEN] ?? this.#activeContext?.apiToken;
|
|
989
|
+
if (!effectiveUrl || !effectiveToken) return { status: "unknown" };
|
|
990
|
+
|
|
991
|
+
// Ad-hoc mode: caller is validating credentials that DIFFER from the active/effective
|
|
992
|
+
// ones — e.g., `/context show <other>` passes a non-active context's apiUrl/apiToken.
|
|
993
|
+
// In that case, do NOT touch the cached auth state — getStatus() would otherwise report
|
|
994
|
+
// the active context's identity with some other context's latency/status.
|
|
995
|
+
//
|
|
996
|
+
// `/context show` on the ACTIVE context (and `/context show` with no name, which resolves
|
|
997
|
+
// to the active name) also passes explicit creds via handleShow, but those creds match
|
|
998
|
+
// the active/effective ones, so we DO want to refresh the cache — a user running
|
|
999
|
+
// /context show on the active context is explicitly requesting a fresh validation.
|
|
1000
|
+
const activeUrl = process.env[XCSH_API_URL] ?? this.#activeContext?.apiUrl;
|
|
1001
|
+
const activeToken = process.env[XCSH_API_TOKEN] ?? this.#activeContext?.apiToken;
|
|
1002
|
+
const adHoc =
|
|
1003
|
+
(options?.apiUrl !== undefined && options.apiUrl !== activeUrl) ||
|
|
1004
|
+
(options?.apiToken !== undefined && options.apiToken !== activeToken);
|
|
1005
|
+
|
|
1006
|
+
const url = `${effectiveUrl}/api/web/namespaces`;
|
|
1007
|
+
const timeout = options?.timeoutMs ?? 3000;
|
|
1008
|
+
const checkedAt = Date.now();
|
|
1009
|
+
try {
|
|
1010
|
+
const start = performance.now();
|
|
1011
|
+
const response = await fetch(url, {
|
|
1012
|
+
method: "GET",
|
|
1013
|
+
headers: { Authorization: `APIToken ${effectiveToken}`, Accept: "application/json" },
|
|
1014
|
+
signal: AbortSignal.timeout(timeout),
|
|
1015
|
+
redirect: "manual",
|
|
1016
|
+
});
|
|
1017
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
1018
|
+
if (!adHoc) {
|
|
1019
|
+
this.#lastAuthLatencyMs = latencyMs;
|
|
1020
|
+
this.#lastAuthCheckedAt = checkedAt;
|
|
1021
|
+
}
|
|
1022
|
+
if (response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400)) {
|
|
1023
|
+
if (!adHoc) this.#authStatus = "offline";
|
|
1024
|
+
return { status: "offline", latencyMs, errorClass: "url_not_found" };
|
|
1025
|
+
}
|
|
1026
|
+
if (response.ok) {
|
|
1027
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1028
|
+
if (!contentType.includes("application/json")) {
|
|
1029
|
+
if (!adHoc) this.#authStatus = "offline";
|
|
1030
|
+
return { status: "offline", latencyMs, errorClass: "url_not_found" };
|
|
1031
|
+
}
|
|
1032
|
+
if (!adHoc) this.#authStatus = "connected";
|
|
1033
|
+
return { status: "connected", latencyMs };
|
|
1034
|
+
}
|
|
1035
|
+
if (response.status === 401 || response.status === 403) {
|
|
1036
|
+
if (!adHoc) this.#authStatus = "auth_error";
|
|
1037
|
+
return { status: "auth_error", latencyMs, errorClass: "credential" };
|
|
1038
|
+
}
|
|
1039
|
+
// 5xx, 429, etc. — server reachable but unhealthy; treat as offline so startup retry fires
|
|
1040
|
+
if (!adHoc) this.#authStatus = "offline";
|
|
1041
|
+
return { status: "offline", latencyMs, errorClass: "network" };
|
|
1042
|
+
} catch {
|
|
1043
|
+
if (!adHoc) {
|
|
1044
|
+
this.#lastAuthLatencyMs = Date.now() - checkedAt;
|
|
1045
|
+
this.#lastAuthCheckedAt = checkedAt;
|
|
1046
|
+
this.#authStatus = "offline";
|
|
1047
|
+
}
|
|
1048
|
+
return { status: "offline", errorClass: "network" };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Validate credentials for a named context without switching the active one.
|
|
1054
|
+
* Uses validateToken's ad-hoc branch (explicit apiUrl + apiToken), so no
|
|
1055
|
+
* cached auth state, namespace cache, or active context is mutated.
|
|
1056
|
+
*
|
|
1057
|
+
* Throws ContextError when the name is invalid, the context is missing, or
|
|
1058
|
+
* the context's schema version is incompatible. Auth failure is not thrown:
|
|
1059
|
+
* it is returned as ValidationResult.status = "auth_error" / "offline".
|
|
1060
|
+
*/
|
|
1061
|
+
async validateContextByName(name: string): Promise<ValidationResult> {
|
|
1062
|
+
this.#validateContextName(name);
|
|
1063
|
+
const context = this.#readContext(name);
|
|
1064
|
+
if (!context) {
|
|
1065
|
+
throw new ContextError(`Context '${name}' not found.`, name);
|
|
1066
|
+
}
|
|
1067
|
+
this.#assertCompatibleVersion(context);
|
|
1068
|
+
const { status, latencyMs, errorClass } = await this.validateToken({
|
|
1069
|
+
apiUrl: context.apiUrl,
|
|
1070
|
+
apiToken: context.apiToken,
|
|
1071
|
+
});
|
|
1072
|
+
return { context, status, latencyMs, errorClass };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
setNamespace(namespace: string): void {
|
|
1076
|
+
if (!this.#activeContext) {
|
|
1077
|
+
throw new ContextError("No active context. Run `/context activate <name>` to select one.");
|
|
1078
|
+
}
|
|
1079
|
+
this.#activeContext = { ...this.#activeContext, defaultNamespace: namespace };
|
|
1080
|
+
// Re-apply settings with the new namespace
|
|
1081
|
+
this.#applyToSettings(this.#activeContext);
|
|
1082
|
+
this.#credentialSource = hasEnvOverride() ? "mixed" : "context";
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
getStatus(): ContextStatus {
|
|
1086
|
+
const url = process.env[XCSH_API_URL] ?? this.#activeContext?.apiUrl ?? null;
|
|
1087
|
+
const tenant = url ? deriveTenantFromUrl(url) : null;
|
|
1088
|
+
return {
|
|
1089
|
+
activeContextName: this.#activeContext?.name ?? null,
|
|
1090
|
+
activeContextUrl: url,
|
|
1091
|
+
activeContextTenant: tenant,
|
|
1092
|
+
activeContextNamespace: process.env[XCSH_NAMESPACE] ?? this.#activeContext?.defaultNamespace ?? null,
|
|
1093
|
+
credentialSource: this.#credentialSource,
|
|
1094
|
+
authStatus: this.#authStatus,
|
|
1095
|
+
isConfigured: this.#credentialSource !== "none",
|
|
1096
|
+
authLatencyMs: this.#lastAuthLatencyMs,
|
|
1097
|
+
authCheckedAt: this.#lastAuthCheckedAt,
|
|
1098
|
+
tokenHealth: this.#computeTokenHealth(),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Sync list of env var keys on the active context, sorted. [] if no active context. */
|
|
1103
|
+
getActiveEnvKeys(): string[] {
|
|
1104
|
+
return Object.keys(this.#activeContext?.env ?? {}).sort();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** Sync set of sensitive env var keys on the active context. Empty set if none. */
|
|
1108
|
+
getActiveSensitiveKeys(): ReadonlySet<string> {
|
|
1109
|
+
return new Set(this.#activeContext?.sensitiveKeys ?? []);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Sync list of known context names, sorted. [] before the first listContexts()/loadActive(). */
|
|
1113
|
+
listContextNamesCached(): string[] {
|
|
1114
|
+
return this.#contextsCache.map(p => p.name);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Sync hint for a context name. Used by the `/context activate` completion
|
|
1119
|
+
* to display the tenant URL and a schema-incompatibility badge.
|
|
1120
|
+
* Returns null if the name is not in the cache.
|
|
1121
|
+
* `incompatible` is always set; `schemaVersion` is set only when incompatible.
|
|
1122
|
+
*/
|
|
1123
|
+
getContextHint(name: string): { apiUrl?: string; incompatible: boolean; schemaVersion?: number } | null {
|
|
1124
|
+
const context = this.#contextsCache.find(p => p.name === name);
|
|
1125
|
+
if (!context) return null;
|
|
1126
|
+
const version = context.version;
|
|
1127
|
+
const incompatible = version !== undefined && version > CURRENT_SCHEMA_VERSION;
|
|
1128
|
+
return {
|
|
1129
|
+
apiUrl: context.apiUrl,
|
|
1130
|
+
incompatible,
|
|
1131
|
+
...(incompatible ? { schemaVersion: version } : {}),
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** Sync namespace names from the most recent successful validateToken response, sorted. */
|
|
1136
|
+
getCachedNamespaces(): string[] {
|
|
1137
|
+
return [...this.#namespacesCache];
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
getActiveContextSkillConfig(): { skillDirs: string[]; includeSkills: string[]; excludeSkills: string[] } {
|
|
1141
|
+
const ctx = this.#activeContext;
|
|
1142
|
+
return {
|
|
1143
|
+
skillDirs: ctx?.knowledgeSources?.filter(s => s.type === "skill-dir").map(s => s.url) ?? [],
|
|
1144
|
+
includeSkills: ctx?.includeSkills ?? [],
|
|
1145
|
+
excludeSkills: ctx?.excludeSkills ?? [],
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
maskToken(token: string): string {
|
|
1150
|
+
if (token.length <= 4) return "****";
|
|
1151
|
+
return `...${token.slice(-4)}`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// --- Private helpers ---
|
|
1155
|
+
|
|
1156
|
+
#atomicWrite(filePath: string, content: string): void {
|
|
1157
|
+
const tmpPath = `${filePath}.tmp`;
|
|
1158
|
+
// Force 0o600 on the tmp file so the atomic rename produces a
|
|
1159
|
+
// destination with credential-file permissions. Without this, the
|
|
1160
|
+
// tmp inherits process umask (typically 0644), fs.renameSync carries
|
|
1161
|
+
// those permissions onto the destination, and any context JSON
|
|
1162
|
+
// updated through this helper (setEnvVars, unsetEnvVars, import
|
|
1163
|
+
// overwrite) ends up world-readable even though createContext
|
|
1164
|
+
// explicitly writes at 0o600. active_context pointer is also
|
|
1165
|
+
// tightened — it names the context but carries no credentials, so
|
|
1166
|
+
// 0o600 is strictly no worse.
|
|
1167
|
+
fs.writeFileSync(tmpPath, content, { mode: 0o600 });
|
|
1168
|
+
fs.renameSync(tmpPath, filePath);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
#isValidContextName(name: string): boolean {
|
|
1172
|
+
return /^[a-zA-Z0-9_-]{1,64}$/.test(name);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
#validateContextName(name: string): void {
|
|
1176
|
+
if (!this.#isValidContextName(name)) {
|
|
1177
|
+
throw new ContextError(
|
|
1178
|
+
`Invalid context name: '${name}'. Names must be alphanumeric with dashes/underscores, max 64 chars.`,
|
|
1179
|
+
name,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
#assertNotReserved(name: string): void {
|
|
1185
|
+
if (RESERVED_CONTEXT_NAMES.has(name.toLowerCase())) {
|
|
1186
|
+
throw new ContextError(
|
|
1187
|
+
`Context name '${name}' conflicts with a /context subcommand. Choose a different name.`,
|
|
1188
|
+
name,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
#assertCompatibleVersion(context: XCSHContext): void {
|
|
1194
|
+
if (context.version !== undefined && context.version > CURRENT_SCHEMA_VERSION) {
|
|
1195
|
+
throw new ContextError(
|
|
1196
|
+
`Context '${context.name}' uses schema version ${context.version}, but this version of xcsh only supports version ${CURRENT_SCHEMA_VERSION}. Upgrade xcsh to use this context, or run \`/context create\` to create a new one.`,
|
|
1197
|
+
context.name,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
#readActiveContextName(): string | null {
|
|
1203
|
+
try {
|
|
1204
|
+
if (!fs.existsSync(this.activeContextPath)) return null;
|
|
1205
|
+
const name = fs.readFileSync(this.activeContextPath, "utf-8").trim();
|
|
1206
|
+
if (!name) return null;
|
|
1207
|
+
// Validate to prevent path traversal from crafted active_context files
|
|
1208
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name)) {
|
|
1209
|
+
logger.warn("XCSH active_context contains invalid name", { name });
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
return name;
|
|
1213
|
+
} catch {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Field-shape check for a parsed context object. Returns a normalized
|
|
1220
|
+
* XCSHContext when obj passes the same rules #readContext enforces on disk
|
|
1221
|
+
* reads, or null when a required field is missing/wrong-typed.
|
|
1222
|
+
*
|
|
1223
|
+
* Used by #readContext (canonical name = filename) and by importContexts
|
|
1224
|
+
* (canonical name = obj.name, which the caller must already have validated
|
|
1225
|
+
* via #isValidContextName).
|
|
1226
|
+
*
|
|
1227
|
+
* Side effect: logger.warn on failure, matching #readContext's original
|
|
1228
|
+
* behavior so existing log-assertion tests continue to pass.
|
|
1229
|
+
*/
|
|
1230
|
+
#validateContextShape(obj: unknown, canonicalName: string): XCSHContext | null {
|
|
1231
|
+
if (!obj || typeof obj !== "object") {
|
|
1232
|
+
logger.warn("XCSH context is not an object", { name: canonicalName });
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
const parsed = obj as Record<string, unknown>;
|
|
1236
|
+
|
|
1237
|
+
if (
|
|
1238
|
+
!parsed.apiUrl ||
|
|
1239
|
+
typeof parsed.apiUrl !== "string" ||
|
|
1240
|
+
!parsed.apiToken ||
|
|
1241
|
+
typeof parsed.apiToken !== "string"
|
|
1242
|
+
) {
|
|
1243
|
+
logger.warn("XCSH context missing or invalid required fields", { name: canonicalName });
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
if (parsed.defaultNamespace && typeof parsed.defaultNamespace !== "string") {
|
|
1247
|
+
logger.warn("XCSH context has non-string defaultNamespace", { name: canonicalName });
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
let env: Record<string, string> | undefined;
|
|
1252
|
+
if (parsed.env && typeof parsed.env === "object" && !Array.isArray(parsed.env)) {
|
|
1253
|
+
env = {};
|
|
1254
|
+
for (const [k, v] of Object.entries(parsed.env)) {
|
|
1255
|
+
if (typeof v !== "string") continue;
|
|
1256
|
+
if (RESERVED_ENV_KEYS.has(k)) {
|
|
1257
|
+
// Resolve the corresponding top-level field to detect value mismatches
|
|
1258
|
+
let topLevelValue: string | undefined;
|
|
1259
|
+
switch (k) {
|
|
1260
|
+
case XCSH_NAMESPACE:
|
|
1261
|
+
topLevelValue = typeof parsed.defaultNamespace === "string" ? parsed.defaultNamespace : undefined;
|
|
1262
|
+
break;
|
|
1263
|
+
case XCSH_API_URL:
|
|
1264
|
+
topLevelValue = typeof parsed.apiUrl === "string" ? parsed.apiUrl : undefined;
|
|
1265
|
+
break;
|
|
1266
|
+
case XCSH_API_TOKEN:
|
|
1267
|
+
topLevelValue = typeof parsed.apiToken === "string" ? parsed.apiToken : undefined;
|
|
1268
|
+
break;
|
|
1269
|
+
default:
|
|
1270
|
+
topLevelValue = undefined;
|
|
1271
|
+
break; // XCSH_TENANT: derived, no stored top-level field
|
|
1272
|
+
}
|
|
1273
|
+
// Warn on mismatch OR when there is no top-level field to compare (XCSH_TENANT)
|
|
1274
|
+
if (topLevelValue === undefined || v !== topLevelValue) {
|
|
1275
|
+
logger.warn("XCSH context env contains reserved key — stripping", {
|
|
1276
|
+
name: canonicalName,
|
|
1277
|
+
key: k,
|
|
1278
|
+
envValue: SECRET_ENV_PATTERNS.test(k) ? "[redacted]" : v,
|
|
1279
|
+
topLevelValue: SECRET_ENV_PATTERNS.test(k) ? "[redacted]" : (topLevelValue ?? "(derived)"),
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
env[k] = v;
|
|
1285
|
+
}
|
|
1286
|
+
if (Object.keys(env).length === 0) env = undefined;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
let sensitiveKeys: string[] | undefined;
|
|
1290
|
+
if (Array.isArray(parsed.sensitiveKeys) && env) {
|
|
1291
|
+
const filtered = parsed.sensitiveKeys.filter((k: unknown): k is string => typeof k === "string" && k in env);
|
|
1292
|
+
sensitiveKeys = filtered.length > 0 ? filtered : undefined;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
let knowledgeSources: KnowledgeSource[] | undefined;
|
|
1296
|
+
if (Array.isArray(parsed.knowledgeSources)) {
|
|
1297
|
+
const validTypes = new Set(["llms-txt", "skill-dir", "docs-site"]);
|
|
1298
|
+
const filtered = (parsed.knowledgeSources as unknown[]).filter((s): s is KnowledgeSource => {
|
|
1299
|
+
if (!s || typeof s !== "object") return false;
|
|
1300
|
+
const entry = s as Record<string, unknown>;
|
|
1301
|
+
if (typeof entry.url !== "string") return false;
|
|
1302
|
+
if ("type" in entry && !validTypes.has(entry.type as string)) return false;
|
|
1303
|
+
return true;
|
|
1304
|
+
});
|
|
1305
|
+
knowledgeSources = filtered.length > 0 ? filtered : undefined;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
let includeSkills: string[] | undefined;
|
|
1309
|
+
if (Array.isArray(parsed.includeSkills)) {
|
|
1310
|
+
const filtered = (parsed.includeSkills as unknown[]).filter((s): s is string => typeof s === "string");
|
|
1311
|
+
includeSkills = filtered.length > 0 ? filtered : undefined;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
let excludeSkills: string[] | undefined;
|
|
1315
|
+
if (Array.isArray(parsed.excludeSkills)) {
|
|
1316
|
+
const filtered = (parsed.excludeSkills as unknown[]).filter((s): s is string => typeof s === "string");
|
|
1317
|
+
excludeSkills = filtered.length > 0 ? filtered : undefined;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return {
|
|
1321
|
+
name: canonicalName,
|
|
1322
|
+
apiUrl: parsed.apiUrl,
|
|
1323
|
+
apiToken: parsed.apiToken,
|
|
1324
|
+
defaultNamespace: typeof parsed.defaultNamespace === "string" ? parsed.defaultNamespace : "default",
|
|
1325
|
+
env,
|
|
1326
|
+
sensitiveKeys,
|
|
1327
|
+
knowledgeSources,
|
|
1328
|
+
includeSkills,
|
|
1329
|
+
excludeSkills,
|
|
1330
|
+
version: typeof parsed.version === "number" ? parsed.version : undefined,
|
|
1331
|
+
metadata:
|
|
1332
|
+
parsed.metadata && typeof parsed.metadata === "object" && !Array.isArray(parsed.metadata)
|
|
1333
|
+
? (parsed.metadata as XCSHContext["metadata"])
|
|
1334
|
+
: undefined,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
#readContext(name: string): XCSHContext | null {
|
|
1339
|
+
const filePath = path.join(this.contextsDir, `${name}.json`);
|
|
1340
|
+
try {
|
|
1341
|
+
if (!fs.existsSync(filePath)) {
|
|
1342
|
+
logger.warn("XCSH context file not found", { name, path: filePath });
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1346
|
+
const parsed = JSON.parse(content);
|
|
1347
|
+
const context = this.#validateContextShape(parsed, name);
|
|
1348
|
+
// Heal pre-existing files whose apiUrl carries a path/query/trailing slash.
|
|
1349
|
+
if (context && typeof context.apiUrl === "string") {
|
|
1350
|
+
return { ...context, apiUrl: normalizeApiUrl(context.apiUrl) };
|
|
1351
|
+
}
|
|
1352
|
+
return context;
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
logger.warn("XCSH context read error", { name, error: String(err) });
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
#listContextFiles(): string[] {
|
|
1360
|
+
try {
|
|
1361
|
+
if (!fs.existsSync(this.contextsDir)) return [];
|
|
1362
|
+
return fs.readdirSync(this.contextsDir).filter(f => f.endsWith(".json"));
|
|
1363
|
+
} catch {
|
|
1364
|
+
return [];
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
#applyToSettings(context: XCSHContext): void {
|
|
1369
|
+
// Per-field merge: skip any key already in process.env (subprocess inherits
|
|
1370
|
+
// it directly), inject context values for the rest. This avoids both
|
|
1371
|
+
// overriding explicit env vars AND losing context values for unset keys.
|
|
1372
|
+
const existing = (Settings.instance.get("bash.environment") ?? {}) as Record<string, string>;
|
|
1373
|
+
// Preserve non-XCSH keys (user-defined HTTP_PROXY, PATH, etc.) but clear
|
|
1374
|
+
// all XCSH_* keys to prevent stale credentials leaking across context switches
|
|
1375
|
+
const merged: Record<string, string> = {};
|
|
1376
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
1377
|
+
if (!key.startsWith("XCSH_")) merged[key] = value;
|
|
1378
|
+
}
|
|
1379
|
+
if (!process.env[XCSH_API_URL]) merged[XCSH_API_URL] = context.apiUrl;
|
|
1380
|
+
if (!process.env[XCSH_API_TOKEN]) merged[XCSH_API_TOKEN] = context.apiToken;
|
|
1381
|
+
if (!process.env[XCSH_NAMESPACE]) merged[XCSH_NAMESPACE] = context.defaultNamespace;
|
|
1382
|
+
|
|
1383
|
+
// Auto-derive XCSH_TENANT from first hostname label of apiUrl
|
|
1384
|
+
if (!process.env[XCSH_TENANT]) {
|
|
1385
|
+
const tenant = deriveTenantFromUrl(context.apiUrl);
|
|
1386
|
+
if (tenant) merged[XCSH_TENANT] = tenant;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Inject context profile name for API tool identity surfacing
|
|
1390
|
+
merged[XCSH_CONTEXT_NAME] = context.name;
|
|
1391
|
+
|
|
1392
|
+
// Inject additional env vars from context.env. Allowlist: only XCSH_-namespaced
|
|
1393
|
+
// non-reserved keys may reach the subprocess — a project-local context file is
|
|
1394
|
+
// untrusted input, so anything outside the XCSH_ namespace (LD_PRELOAD,
|
|
1395
|
+
// NODE_OPTIONS, PATH, …) is refused and can never run code.
|
|
1396
|
+
if (context.env) {
|
|
1397
|
+
for (const [key, value] of Object.entries(context.env)) {
|
|
1398
|
+
if (isInjectableContextEnvKey(key) && !process.env[key]) merged[key] = value;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
Settings.instance.override("bash.environment", merged);
|
|
1403
|
+
Settings.instance.override("xcsh.sensitiveKeys", context.sensitiveKeys ?? []);
|
|
1404
|
+
|
|
1405
|
+
// Notify listeners (e.g. obfuscator refresh) about the context change.
|
|
1406
|
+
for (const cb of ContextService.#onContextChangeListeners) {
|
|
1407
|
+
cb(context);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|