@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.
Files changed (899) hide show
  1. package/CHANGELOG.md +7073 -0
  2. package/README.md +14 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +109 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/custom-tools/todo/index.ts +206 -0
  7. package/examples/extensions/README.md +143 -0
  8. package/examples/extensions/api-demo.ts +89 -0
  9. package/examples/extensions/chalk-logger.ts +25 -0
  10. package/examples/extensions/hello.ts +32 -0
  11. package/examples/extensions/pirate.ts +43 -0
  12. package/examples/extensions/plan-mode.ts +551 -0
  13. package/examples/extensions/reload-runtime.ts +37 -0
  14. package/examples/extensions/todo.ts +296 -0
  15. package/examples/extensions/tools.ts +145 -0
  16. package/examples/extensions/with-deps/index.ts +36 -0
  17. package/examples/extensions/with-deps/package-lock.json +31 -0
  18. package/examples/extensions/with-deps/package.json +16 -0
  19. package/examples/hooks/README.md +56 -0
  20. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  21. package/examples/hooks/confirm-destructive.ts +58 -0
  22. package/examples/hooks/custom-compaction.ts +115 -0
  23. package/examples/hooks/dirty-repo-guard.ts +51 -0
  24. package/examples/hooks/file-trigger.ts +40 -0
  25. package/examples/hooks/git-checkpoint.ts +52 -0
  26. package/examples/hooks/handoff.ts +149 -0
  27. package/examples/hooks/permission-gate.ts +33 -0
  28. package/examples/hooks/protected-paths.ts +29 -0
  29. package/examples/hooks/qna.ts +118 -0
  30. package/examples/hooks/status-line.ts +39 -0
  31. package/examples/sdk/01-minimal.ts +21 -0
  32. package/examples/sdk/02-custom-model.ts +49 -0
  33. package/examples/sdk/03-custom-prompt.ts +43 -0
  34. package/examples/sdk/04-skills.ts +43 -0
  35. package/examples/sdk/06-extensions.ts +80 -0
  36. package/examples/sdk/06-hooks.ts +61 -0
  37. package/examples/sdk/07-context-files.ts +35 -0
  38. package/examples/sdk/08-prompt-templates.ts +41 -0
  39. package/examples/sdk/08-slash-commands.ts +46 -0
  40. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  41. package/examples/sdk/11-sessions.ts +47 -0
  42. package/examples/sdk/README.md +172 -0
  43. package/package.json +539 -0
  44. package/scripts/build-info/resolvers.ts +51 -0
  45. package/scripts/capture-ax-fixture.ts +84 -0
  46. package/scripts/capture-login-fixture.ts +30 -0
  47. package/scripts/check-workflow-field-coverage.ts +101 -0
  48. package/scripts/extension-uat-harness.ts +340 -0
  49. package/scripts/format-prompts.ts +68 -0
  50. package/scripts/generate-api-spec-index.ts +580 -0
  51. package/scripts/generate-branding-index.ts +81 -0
  52. package/scripts/generate-build-info.ts +106 -0
  53. package/scripts/generate-console-catalog.ts +88 -0
  54. package/scripts/generate-console-field-metadata.ts +74 -0
  55. package/scripts/generate-docs-index.ts +40 -0
  56. package/scripts/generate-template.ts +32 -0
  57. package/scripts/generate-terraform-index.ts +81 -0
  58. package/scripts/mermaid-gallery.ts +132 -0
  59. package/scripts/uat-matrix-modalities.ts +451 -0
  60. package/scripts/uat-matrix-report.ts +175 -0
  61. package/scripts/uat-matrix.ts +353 -0
  62. package/src/async/index.ts +2 -0
  63. package/src/async/job-manager.ts +416 -0
  64. package/src/async/support.ts +5 -0
  65. package/src/autoresearch/apply-contract-to-state.ts +1 -0
  66. package/src/autoresearch/command-resume.md +17 -0
  67. package/src/autoresearch/contract.ts +205 -0
  68. package/src/autoresearch/dashboard.ts +374 -0
  69. package/src/autoresearch/git.ts +191 -0
  70. package/src/autoresearch/helpers.ts +328 -0
  71. package/src/autoresearch/index.ts +402 -0
  72. package/src/autoresearch/prompt.md +236 -0
  73. package/src/autoresearch/resume-message.md +16 -0
  74. package/src/autoresearch/state.ts +303 -0
  75. package/src/autoresearch/tools/init-experiment.ts +400 -0
  76. package/src/autoresearch/tools/log-experiment.ts +793 -0
  77. package/src/autoresearch/tools/run-experiment.ts +679 -0
  78. package/src/autoresearch/types.ts +114 -0
  79. package/src/browser/acquire.ts +249 -0
  80. package/src/browser/actions.ts +126 -0
  81. package/src/browser/auth.ts +176 -0
  82. package/src/browser/ax.ts +134 -0
  83. package/src/browser/cdp-core.ts +45 -0
  84. package/src/browser/cdp-page-actions.ts +51 -0
  85. package/src/browser/chrome-locate.ts +66 -0
  86. package/src/browser/dom-context.ts +89 -0
  87. package/src/browser/dt-context.ts +100 -0
  88. package/src/browser/extension-bridge.ts +182 -0
  89. package/src/browser/extension-page-actions.ts +397 -0
  90. package/src/browser/extension-provider.ts +373 -0
  91. package/src/browser/index.ts +17 -0
  92. package/src/browser/input-commit.ts +74 -0
  93. package/src/browser/native-messaging.ts +20 -0
  94. package/src/browser/page-actions.ts +18 -0
  95. package/src/browser/provider.ts +155 -0
  96. package/src/browser/resolver.ts +126 -0
  97. package/src/browser/selector.ts +40 -0
  98. package/src/bun-imports.d.ts +22 -0
  99. package/src/capability/context-file.ts +43 -0
  100. package/src/capability/extension-module.ts +34 -0
  101. package/src/capability/extension.ts +47 -0
  102. package/src/capability/fs.ts +107 -0
  103. package/src/capability/hook.ts +40 -0
  104. package/src/capability/index.ts +438 -0
  105. package/src/capability/instruction.ts +37 -0
  106. package/src/capability/mcp.ts +74 -0
  107. package/src/capability/prompt.ts +35 -0
  108. package/src/capability/rule.ts +224 -0
  109. package/src/capability/settings.ts +34 -0
  110. package/src/capability/skill.ts +50 -0
  111. package/src/capability/slash-command.ts +40 -0
  112. package/src/capability/ssh.ts +41 -0
  113. package/src/capability/system-prompt.ts +34 -0
  114. package/src/capability/tool.ts +38 -0
  115. package/src/capability/types.ts +168 -0
  116. package/src/cli/agents-cli.ts +138 -0
  117. package/src/cli/args.ts +286 -0
  118. package/src/cli/chrome-cli.ts +88 -0
  119. package/src/cli/classify-install-target.ts +50 -0
  120. package/src/cli/commands/init-xdg.ts +27 -0
  121. package/src/cli/config-cli.ts +418 -0
  122. package/src/cli/file-processor.ts +121 -0
  123. package/src/cli/grep-cli.ts +160 -0
  124. package/src/cli/grievances-cli.ts +78 -0
  125. package/src/cli/initial-message.ts +58 -0
  126. package/src/cli/jupyter-cli.ts +106 -0
  127. package/src/cli/list-models.ts +128 -0
  128. package/src/cli/plugin-cli.ts +964 -0
  129. package/src/cli/read-cli.ts +68 -0
  130. package/src/cli/session-picker.ts +52 -0
  131. package/src/cli/setup-cli.ts +479 -0
  132. package/src/cli/shell-cli.ts +174 -0
  133. package/src/cli/ssh-cli.ts +179 -0
  134. package/src/cli/stats-cli.ts +182 -0
  135. package/src/cli/update-cli.ts +446 -0
  136. package/src/cli/web-search-cli.ts +203 -0
  137. package/src/cli.ts +123 -0
  138. package/src/commands/agents.ts +57 -0
  139. package/src/commands/chrome-host.ts +71 -0
  140. package/src/commands/chrome.ts +28 -0
  141. package/src/commands/commit.ts +36 -0
  142. package/src/commands/config.ts +51 -0
  143. package/src/commands/grep.ts +48 -0
  144. package/src/commands/grievances.ts +20 -0
  145. package/src/commands/jupyter.ts +32 -0
  146. package/src/commands/launch.ts +144 -0
  147. package/src/commands/plugin.ts +78 -0
  148. package/src/commands/read.ts +33 -0
  149. package/src/commands/setup.ts +42 -0
  150. package/src/commands/shell.ts +29 -0
  151. package/src/commands/ssh.ts +60 -0
  152. package/src/commands/stats.ts +29 -0
  153. package/src/commands/update.ts +21 -0
  154. package/src/commands/web-search.ts +46 -0
  155. package/src/commit/agentic/agent.ts +314 -0
  156. package/src/commit/agentic/fallback.ts +96 -0
  157. package/src/commit/agentic/index.ts +354 -0
  158. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  159. package/src/commit/agentic/prompts/session-user.md +25 -0
  160. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  161. package/src/commit/agentic/prompts/system.md +38 -0
  162. package/src/commit/agentic/state.ts +60 -0
  163. package/src/commit/agentic/tools/analyze-file.ts +127 -0
  164. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  165. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  166. package/src/commit/agentic/tools/git-overview.ts +81 -0
  167. package/src/commit/agentic/tools/index.ts +54 -0
  168. package/src/commit/agentic/tools/propose-changelog.ts +139 -0
  169. package/src/commit/agentic/tools/propose-commit.ts +122 -0
  170. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  171. package/src/commit/agentic/tools/schemas.ts +31 -0
  172. package/src/commit/agentic/tools/split-commit.ts +251 -0
  173. package/src/commit/agentic/topo-sort.ts +44 -0
  174. package/src/commit/agentic/trivial.ts +51 -0
  175. package/src/commit/agentic/validation.ts +183 -0
  176. package/src/commit/analysis/conventional.ts +122 -0
  177. package/src/commit/analysis/index.ts +4 -0
  178. package/src/commit/analysis/scope.ts +242 -0
  179. package/src/commit/analysis/summary.ts +105 -0
  180. package/src/commit/analysis/validation.ts +66 -0
  181. package/src/commit/changelog/detect.ts +40 -0
  182. package/src/commit/changelog/generate.ts +101 -0
  183. package/src/commit/changelog/index.ts +234 -0
  184. package/src/commit/changelog/parse.ts +44 -0
  185. package/src/commit/cli.ts +85 -0
  186. package/src/commit/git/diff.ts +148 -0
  187. package/src/commit/index.ts +5 -0
  188. package/src/commit/map-reduce/index.ts +69 -0
  189. package/src/commit/map-reduce/map-phase.ts +193 -0
  190. package/src/commit/map-reduce/reduce-phase.ts +108 -0
  191. package/src/commit/map-reduce/utils.ts +9 -0
  192. package/src/commit/message.ts +11 -0
  193. package/src/commit/model-selection.ts +66 -0
  194. package/src/commit/pipeline.ts +242 -0
  195. package/src/commit/prompts/analysis-system.md +148 -0
  196. package/src/commit/prompts/analysis-user.md +38 -0
  197. package/src/commit/prompts/changelog-system.md +50 -0
  198. package/src/commit/prompts/changelog-user.md +18 -0
  199. package/src/commit/prompts/file-observer-system.md +24 -0
  200. package/src/commit/prompts/file-observer-user.md +8 -0
  201. package/src/commit/prompts/reduce-system.md +50 -0
  202. package/src/commit/prompts/reduce-user.md +17 -0
  203. package/src/commit/prompts/summary-retry.md +3 -0
  204. package/src/commit/prompts/summary-system.md +38 -0
  205. package/src/commit/prompts/summary-user.md +13 -0
  206. package/src/commit/prompts/types-description.md +2 -0
  207. package/src/commit/types.ts +118 -0
  208. package/src/commit/utils/exclusions.ts +42 -0
  209. package/src/commit/utils.ts +44 -0
  210. package/src/config/auto-config.ts +698 -0
  211. package/src/config/context-schema.json +115 -0
  212. package/src/config/file-lock.ts +121 -0
  213. package/src/config/keybindings.ts +493 -0
  214. package/src/config/mcp-schema.json +230 -0
  215. package/src/config/model-equivalence.ts +675 -0
  216. package/src/config/model-registry.ts +2265 -0
  217. package/src/config/model-resolver.ts +1289 -0
  218. package/src/config/prompt-templates.ts +271 -0
  219. package/src/config/resolve-config-value.ts +110 -0
  220. package/src/config/settings-schema.ts +2155 -0
  221. package/src/config/settings.ts +730 -0
  222. package/src/config.ts +418 -0
  223. package/src/cursor.ts +323 -0
  224. package/src/dap/client.ts +674 -0
  225. package/src/dap/config.ts +150 -0
  226. package/src/dap/defaults.json +211 -0
  227. package/src/dap/index.ts +4 -0
  228. package/src/dap/session.ts +1255 -0
  229. package/src/dap/types.ts +600 -0
  230. package/src/debug/index.ts +440 -0
  231. package/src/debug/log-formatting.ts +58 -0
  232. package/src/debug/log-viewer.ts +908 -0
  233. package/src/debug/profiler.ts +158 -0
  234. package/src/debug/report-bundle.ts +365 -0
  235. package/src/debug/system-info.ts +107 -0
  236. package/src/discovery/agents-md.ts +67 -0
  237. package/src/discovery/agents.ts +219 -0
  238. package/src/discovery/builtin.ts +851 -0
  239. package/src/discovery/claude-plugins.ts +297 -0
  240. package/src/discovery/claude.ts +543 -0
  241. package/src/discovery/cline.ts +83 -0
  242. package/src/discovery/codex.ts +535 -0
  243. package/src/discovery/cursor.ts +220 -0
  244. package/src/discovery/gemini.ts +396 -0
  245. package/src/discovery/github.ts +118 -0
  246. package/src/discovery/helpers.ts +934 -0
  247. package/src/discovery/index.ts +79 -0
  248. package/src/discovery/language.ts +46 -0
  249. package/src/discovery/mcp-json.ts +171 -0
  250. package/src/discovery/opencode.ts +393 -0
  251. package/src/discovery/plugin-dir-roots.ts +28 -0
  252. package/src/discovery/ssh.ts +153 -0
  253. package/src/discovery/substitute-plugin-root.ts +29 -0
  254. package/src/discovery/vscode.ts +105 -0
  255. package/src/discovery/windsurf.ts +147 -0
  256. package/src/edit/diff.ts +818 -0
  257. package/src/edit/index.ts +481 -0
  258. package/src/edit/line-hash.ts +67 -0
  259. package/src/edit/modes/chunk.ts +762 -0
  260. package/src/edit/modes/hashline.ts +1366 -0
  261. package/src/edit/modes/patch.ts +1791 -0
  262. package/src/edit/modes/replace.ts +1115 -0
  263. package/src/edit/normalize.ts +375 -0
  264. package/src/edit/renderer.ts +627 -0
  265. package/src/exa/factory.ts +61 -0
  266. package/src/exa/index.ts +27 -0
  267. package/src/exa/mcp-client.ts +293 -0
  268. package/src/exa/render.ts +232 -0
  269. package/src/exa/researcher.ts +47 -0
  270. package/src/exa/search.ts +86 -0
  271. package/src/exa/types.ts +166 -0
  272. package/src/exa/websets.ts +247 -0
  273. package/src/exec/bash-executor.ts +332 -0
  274. package/src/exec/exec.ts +53 -0
  275. package/src/exec/idle-timeout-watchdog.ts +126 -0
  276. package/src/exec/non-interactive-env.ts +48 -0
  277. package/src/export/custom-share.ts +65 -0
  278. package/src/export/html/index.ts +161 -0
  279. package/src/export/html/template.css +1001 -0
  280. package/src/export/html/template.generated.ts +2 -0
  281. package/src/export/html/template.html +46 -0
  282. package/src/export/html/template.js +1950 -0
  283. package/src/export/html/template.macro.ts +24 -0
  284. package/src/export/html/vendor/highlight.min.js +1213 -0
  285. package/src/export/html/vendor/marked.min.js +6 -0
  286. package/src/export/ttsr.ts +434 -0
  287. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +25 -0
  288. package/src/extensibility/custom-commands/bundled/review/index.ts +456 -0
  289. package/src/extensibility/custom-commands/index.ts +2 -0
  290. package/src/extensibility/custom-commands/loader.ts +236 -0
  291. package/src/extensibility/custom-commands/types.ts +111 -0
  292. package/src/extensibility/custom-tools/index.ts +7 -0
  293. package/src/extensibility/custom-tools/loader.ts +238 -0
  294. package/src/extensibility/custom-tools/types.ts +251 -0
  295. package/src/extensibility/custom-tools/wrapper.ts +47 -0
  296. package/src/extensibility/extensions/index.ts +15 -0
  297. package/src/extensibility/extensions/loader.ts +572 -0
  298. package/src/extensibility/extensions/runner.ts +848 -0
  299. package/src/extensibility/extensions/types.ts +1394 -0
  300. package/src/extensibility/extensions/wrapper.ts +191 -0
  301. package/src/extensibility/hooks/index.ts +5 -0
  302. package/src/extensibility/hooks/loader.ts +255 -0
  303. package/src/extensibility/hooks/runner.ts +425 -0
  304. package/src/extensibility/hooks/tool-wrapper.ts +108 -0
  305. package/src/extensibility/hooks/types.ts +826 -0
  306. package/src/extensibility/plugins/doctor.ts +66 -0
  307. package/src/extensibility/plugins/git-url.ts +281 -0
  308. package/src/extensibility/plugins/index.ts +9 -0
  309. package/src/extensibility/plugins/installer.ts +192 -0
  310. package/src/extensibility/plugins/loader.ts +267 -0
  311. package/src/extensibility/plugins/manager.ts +731 -0
  312. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  313. package/src/extensibility/plugins/marketplace/fetcher.ts +318 -0
  314. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  315. package/src/extensibility/plugins/marketplace/manager.ts +781 -0
  316. package/src/extensibility/plugins/marketplace/prerequisites.ts +255 -0
  317. package/src/extensibility/plugins/marketplace/registry.ts +196 -0
  318. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  319. package/src/extensibility/plugins/marketplace/types.ts +201 -0
  320. package/src/extensibility/plugins/parser.ts +105 -0
  321. package/src/extensibility/plugins/types.ts +194 -0
  322. package/src/extensibility/skills.ts +269 -0
  323. package/src/extensibility/slash-commands.ts +246 -0
  324. package/src/extensibility/tool-proxy.ts +25 -0
  325. package/src/extensibility/utils.ts +38 -0
  326. package/src/index.ts +61 -0
  327. package/src/internal-urls/agent-protocol.ts +136 -0
  328. package/src/internal-urls/api-catalog-resolve.ts +495 -0
  329. package/src/internal-urls/api-catalog-types.ts +94 -0
  330. package/src/internal-urls/api-spec-resolve.ts +1036 -0
  331. package/src/internal-urls/api-spec-types.ts +239 -0
  332. package/src/internal-urls/artifact-protocol.ts +97 -0
  333. package/src/internal-urls/branding-index.generated.ts +88 -0
  334. package/src/internal-urls/build-info-runtime.ts +302 -0
  335. package/src/internal-urls/build-info.generated.ts +33 -0
  336. package/src/internal-urls/computer-profile.ts +692 -0
  337. package/src/internal-urls/console-catalog-types.ts +21 -0
  338. package/src/internal-urls/console-catalog.generated.ts +532 -0
  339. package/src/internal-urls/console-field-metadata-types.ts +59 -0
  340. package/src/internal-urls/console-field-metadata.generated.ts +4841 -0
  341. package/src/internal-urls/console-resolve.ts +178 -0
  342. package/src/internal-urls/docs-index.generated.ts +698 -0
  343. package/src/internal-urls/extension-api.md +147 -0
  344. package/src/internal-urls/index.ts +46 -0
  345. package/src/internal-urls/jobs-protocol.ts +119 -0
  346. package/src/internal-urls/json-query.ts +126 -0
  347. package/src/internal-urls/local-protocol.ts +206 -0
  348. package/src/internal-urls/mcp-protocol.ts +156 -0
  349. package/src/internal-urls/memory-protocol.ts +133 -0
  350. package/src/internal-urls/parse.ts +72 -0
  351. package/src/internal-urls/profile-collectors.ts +106 -0
  352. package/src/internal-urls/router.ts +55 -0
  353. package/src/internal-urls/rule-protocol.ts +55 -0
  354. package/src/internal-urls/skill-protocol.ts +111 -0
  355. package/src/internal-urls/terraform-index.generated.ts +1852 -0
  356. package/src/internal-urls/terraform-resolve.ts +184 -0
  357. package/src/internal-urls/terraform-types.ts +45 -0
  358. package/src/internal-urls/types.ts +52 -0
  359. package/src/internal-urls/user-profile.ts +363 -0
  360. package/src/internal-urls/xcsh-protocol.ts +644 -0
  361. package/src/ipy/cancellation.ts +28 -0
  362. package/src/ipy/executor.ts +1155 -0
  363. package/src/ipy/gateway-coordinator.ts +424 -0
  364. package/src/ipy/kernel.ts +1091 -0
  365. package/src/ipy/modules.ts +144 -0
  366. package/src/ipy/prelude.py +849 -0
  367. package/src/ipy/prelude.ts +3 -0
  368. package/src/ipy/runtime.ts +221 -0
  369. package/src/locales/ar.json +512 -0
  370. package/src/locales/de.json +512 -0
  371. package/src/locales/en.json +549 -0
  372. package/src/locales/es.json +512 -0
  373. package/src/locales/fr.json +512 -0
  374. package/src/locales/hi.json +512 -0
  375. package/src/locales/index.ts +29 -0
  376. package/src/locales/it.json +512 -0
  377. package/src/locales/ja.json +512 -0
  378. package/src/locales/ko.json +512 -0
  379. package/src/locales/pt-br.json +512 -0
  380. package/src/locales/th.json +512 -0
  381. package/src/locales/zh-cn.json +512 -0
  382. package/src/locales/zh-tw.json +512 -0
  383. package/src/lsp/client.ts +941 -0
  384. package/src/lsp/clients/biome-client.ts +202 -0
  385. package/src/lsp/clients/index.ts +50 -0
  386. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  387. package/src/lsp/clients/swiftlint-client.ts +120 -0
  388. package/src/lsp/config.ts +418 -0
  389. package/src/lsp/defaults.json +999 -0
  390. package/src/lsp/edits.ts +109 -0
  391. package/src/lsp/index.ts +1746 -0
  392. package/src/lsp/lspmux.ts +233 -0
  393. package/src/lsp/render.ts +692 -0
  394. package/src/lsp/startup-events.ts +13 -0
  395. package/src/lsp/types.ts +444 -0
  396. package/src/lsp/utils.ts +691 -0
  397. package/src/main.ts +944 -0
  398. package/src/mcp/client.ts +482 -0
  399. package/src/mcp/config-writer.ts +225 -0
  400. package/src/mcp/config.ts +365 -0
  401. package/src/mcp/discoverable-tool-metadata.ts +202 -0
  402. package/src/mcp/index.ts +29 -0
  403. package/src/mcp/json-rpc.ts +84 -0
  404. package/src/mcp/loader.ts +124 -0
  405. package/src/mcp/manager.ts +1152 -0
  406. package/src/mcp/oauth-discovery.ts +349 -0
  407. package/src/mcp/oauth-flow.ts +387 -0
  408. package/src/mcp/render.ts +157 -0
  409. package/src/mcp/smithery-auth.ts +104 -0
  410. package/src/mcp/smithery-connect.ts +145 -0
  411. package/src/mcp/smithery-registry.ts +477 -0
  412. package/src/mcp/tool-bridge.ts +417 -0
  413. package/src/mcp/tool-cache.ts +117 -0
  414. package/src/mcp/transports/http.ts +475 -0
  415. package/src/mcp/transports/index.ts +6 -0
  416. package/src/mcp/transports/stdio.ts +325 -0
  417. package/src/mcp/types.ts +423 -0
  418. package/src/memories/index.ts +1115 -0
  419. package/src/memories/storage.ts +577 -0
  420. package/src/modes/acp/acp-agent.ts +1361 -0
  421. package/src/modes/acp/acp-event-mapper.ts +538 -0
  422. package/src/modes/acp/acp-mode.ts +15 -0
  423. package/src/modes/acp/index.ts +2 -0
  424. package/src/modes/components/agent-dashboard.ts +1124 -0
  425. package/src/modes/components/assistant-message.ts +213 -0
  426. package/src/modes/components/bash-execution.ts +345 -0
  427. package/src/modes/components/bordered-loader.ts +41 -0
  428. package/src/modes/components/branch-summary-message.ts +45 -0
  429. package/src/modes/components/btw-panel.ts +103 -0
  430. package/src/modes/components/compaction-summary-message.ts +51 -0
  431. package/src/modes/components/context-add-wizard.ts +534 -0
  432. package/src/modes/components/countdown-timer.ts +75 -0
  433. package/src/modes/components/custom-editor.ts +238 -0
  434. package/src/modes/components/custom-message.ts +91 -0
  435. package/src/modes/components/diff.ts +210 -0
  436. package/src/modes/components/dynamic-border.ts +25 -0
  437. package/src/modes/components/extensions/extension-dashboard.ts +326 -0
  438. package/src/modes/components/extensions/extension-list.ts +492 -0
  439. package/src/modes/components/extensions/index.ts +9 -0
  440. package/src/modes/components/extensions/inspector-panel.ts +317 -0
  441. package/src/modes/components/extensions/state-manager.ts +587 -0
  442. package/src/modes/components/extensions/types.ts +191 -0
  443. package/src/modes/components/footer.ts +262 -0
  444. package/src/modes/components/gutter-block.ts +315 -0
  445. package/src/modes/components/history-search.ts +158 -0
  446. package/src/modes/components/hook-editor.ts +151 -0
  447. package/src/modes/components/hook-input.ts +81 -0
  448. package/src/modes/components/hook-message.ts +100 -0
  449. package/src/modes/components/hook-selector.ts +192 -0
  450. package/src/modes/components/index.ts +36 -0
  451. package/src/modes/components/keybinding-hints.ts +65 -0
  452. package/src/modes/components/login-dialog.ts +164 -0
  453. package/src/modes/components/mcp-add-wizard.ts +1314 -0
  454. package/src/modes/components/model-selector.ts +946 -0
  455. package/src/modes/components/oauth-selector.ts +211 -0
  456. package/src/modes/components/plugin-selector.ts +95 -0
  457. package/src/modes/components/plugin-settings.ts +477 -0
  458. package/src/modes/components/plugins/index.ts +5 -0
  459. package/src/modes/components/plugins/plugin-dashboard.ts +523 -0
  460. package/src/modes/components/plugins/plugin-inspector-pane.ts +84 -0
  461. package/src/modes/components/plugins/plugin-list-pane.ts +109 -0
  462. package/src/modes/components/plugins/state-manager.ts +229 -0
  463. package/src/modes/components/plugins/types.ts +51 -0
  464. package/src/modes/components/plugins/utils.ts +6 -0
  465. package/src/modes/components/python-execution.ts +244 -0
  466. package/src/modes/components/queue-mode-selector.ts +56 -0
  467. package/src/modes/components/read-tool-group.ts +137 -0
  468. package/src/modes/components/session-observer-overlay.ts +483 -0
  469. package/src/modes/components/session-selector.ts +343 -0
  470. package/src/modes/components/settings-defs.ts +568 -0
  471. package/src/modes/components/settings-selector.ts +641 -0
  472. package/src/modes/components/show-images-selector.ts +45 -0
  473. package/src/modes/components/skill-message.ts +90 -0
  474. package/src/modes/components/status-line/context-gradient.ts +64 -0
  475. package/src/modes/components/status-line/git-utils.ts +42 -0
  476. package/src/modes/components/status-line/hex-ansi.ts +29 -0
  477. package/src/modes/components/status-line/index.ts +4 -0
  478. package/src/modes/components/status-line/presets.ts +174 -0
  479. package/src/modes/components/status-line/segments.ts +494 -0
  480. package/src/modes/components/status-line/separators.ts +55 -0
  481. package/src/modes/components/status-line/token-rate.ts +66 -0
  482. package/src/modes/components/status-line/types.ts +96 -0
  483. package/src/modes/components/status-line-segment-editor.ts +361 -0
  484. package/src/modes/components/status-line.ts +729 -0
  485. package/src/modes/components/theme-selector.ts +63 -0
  486. package/src/modes/components/thinking-selector.ts +52 -0
  487. package/src/modes/components/todo-reminder.ts +54 -0
  488. package/src/modes/components/tool-execution.ts +781 -0
  489. package/src/modes/components/tree-selector.ts +908 -0
  490. package/src/modes/components/ttsr-notification.ts +80 -0
  491. package/src/modes/components/user-message-selector.ts +141 -0
  492. package/src/modes/components/user-message.ts +100 -0
  493. package/src/modes/components/visual-truncate.ts +63 -0
  494. package/src/modes/components/welcome-checks.ts +313 -0
  495. package/src/modes/components/welcome.ts +270 -0
  496. package/src/modes/controllers/btw-controller.ts +193 -0
  497. package/src/modes/controllers/chord-routing.ts +37 -0
  498. package/src/modes/controllers/command-controller.ts +1293 -0
  499. package/src/modes/controllers/context-command-controller.ts +61 -0
  500. package/src/modes/controllers/event-controller.ts +743 -0
  501. package/src/modes/controllers/extension-ui-controller.ts +983 -0
  502. package/src/modes/controllers/input-controller.ts +877 -0
  503. package/src/modes/controllers/mcp-command-controller.ts +1940 -0
  504. package/src/modes/controllers/selector-controller.ts +1292 -0
  505. package/src/modes/controllers/ssh-command-controller.ts +421 -0
  506. package/src/modes/index.ts +34 -0
  507. package/src/modes/interactive-mode.ts +1781 -0
  508. package/src/modes/oauth-manual-input.ts +42 -0
  509. package/src/modes/print-mode.ts +196 -0
  510. package/src/modes/prompt-action-autocomplete.ts +231 -0
  511. package/src/modes/rpc/host-tools.ts +186 -0
  512. package/src/modes/rpc/rpc-client.ts +762 -0
  513. package/src/modes/rpc/rpc-mode.ts +877 -0
  514. package/src/modes/rpc/rpc-types.ts +322 -0
  515. package/src/modes/session-observer-registry.ts +146 -0
  516. package/src/modes/shared.ts +30 -0
  517. package/src/modes/theme/defaults/index.ts +7 -0
  518. package/src/modes/theme/defaults/xcsh-dark.json +113 -0
  519. package/src/modes/theme/defaults/xcsh-light.json +115 -0
  520. package/src/modes/theme/mermaid-cache.ts +151 -0
  521. package/src/modes/theme/mermaid-palette.ts +39 -0
  522. package/src/modes/theme/theme-schema.json +553 -0
  523. package/src/modes/theme/theme.ts +2563 -0
  524. package/src/modes/types.ts +273 -0
  525. package/src/modes/utils/hotkeys-markdown.ts +60 -0
  526. package/src/modes/utils/keybinding-matchers.ts +30 -0
  527. package/src/modes/utils/read-group-outcome-aggregator.ts +55 -0
  528. package/src/modes/utils/sanitize-error-message.ts +60 -0
  529. package/src/modes/utils/tools-markdown.ts +28 -0
  530. package/src/modes/utils/ui-helpers.ts +734 -0
  531. package/src/plan-mode/approved-plan.ts +55 -0
  532. package/src/plan-mode/state.ts +6 -0
  533. package/src/priority.json +37 -0
  534. package/src/prompts/agents/designer.md +67 -0
  535. package/src/prompts/agents/explore.md +61 -0
  536. package/src/prompts/agents/frontmatter.md +10 -0
  537. package/src/prompts/agents/init.md +36 -0
  538. package/src/prompts/agents/librarian.md +121 -0
  539. package/src/prompts/agents/plan.md +49 -0
  540. package/src/prompts/agents/reviewer.md +127 -0
  541. package/src/prompts/agents/task.md +16 -0
  542. package/src/prompts/ci-green-request.md +35 -0
  543. package/src/prompts/compaction/branch-summary-context.md +5 -0
  544. package/src/prompts/compaction/branch-summary-preamble.md +2 -0
  545. package/src/prompts/compaction/branch-summary.md +30 -0
  546. package/src/prompts/compaction/compaction-short-summary.md +9 -0
  547. package/src/prompts/compaction/compaction-summary-context.md +5 -0
  548. package/src/prompts/compaction/compaction-summary.md +38 -0
  549. package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
  550. package/src/prompts/compaction/compaction-update-summary.md +45 -0
  551. package/src/prompts/memories/consolidation.md +30 -0
  552. package/src/prompts/memories/read-path.md +11 -0
  553. package/src/prompts/memories/stage_one_input.md +6 -0
  554. package/src/prompts/memories/stage_one_system.md +21 -0
  555. package/src/prompts/review-request.md +70 -0
  556. package/src/prompts/system/agent-creation-architect.md +65 -0
  557. package/src/prompts/system/agent-creation-user.md +6 -0
  558. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
  559. package/src/prompts/system/btw-user.md +8 -0
  560. package/src/prompts/system/commit-message-system.md +2 -0
  561. package/src/prompts/system/custom-system-prompt.md +82 -0
  562. package/src/prompts/system/eager-todo.md +13 -0
  563. package/src/prompts/system/file-operations.md +10 -0
  564. package/src/prompts/system/handoff-document.md +48 -0
  565. package/src/prompts/system/plan-mode-active.md +114 -0
  566. package/src/prompts/system/plan-mode-approved.md +23 -0
  567. package/src/prompts/system/plan-mode-reference.md +14 -0
  568. package/src/prompts/system/plan-mode-subagent.md +34 -0
  569. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  570. package/src/prompts/system/subagent-submit-reminder.md +7 -0
  571. package/src/prompts/system/subagent-system-prompt.md +36 -0
  572. package/src/prompts/system/subagent-user-prompt.md +12 -0
  573. package/src/prompts/system/summarization-system.md +3 -0
  574. package/src/prompts/system/system-prompt.md +698 -0
  575. package/src/prompts/system/title-system.md +2 -0
  576. package/src/prompts/system/ttsr-interrupt.md +7 -0
  577. package/src/prompts/system/web-search.md +28 -0
  578. package/src/prompts/tools/ask.md +28 -0
  579. package/src/prompts/tools/ast-edit.md +48 -0
  580. package/src/prompts/tools/ast-grep.md +54 -0
  581. package/src/prompts/tools/async-result.md +5 -0
  582. package/src/prompts/tools/bash.md +67 -0
  583. package/src/prompts/tools/browser.md +26 -0
  584. package/src/prompts/tools/calculator.md +10 -0
  585. package/src/prompts/tools/cancel-job.md +5 -0
  586. package/src/prompts/tools/catalog-workflow-runner.md +13 -0
  587. package/src/prompts/tools/checkpoint.md +16 -0
  588. package/src/prompts/tools/chunk-edit.md +279 -0
  589. package/src/prompts/tools/debug.md +43 -0
  590. package/src/prompts/tools/display-image.md +23 -0
  591. package/src/prompts/tools/exit-plan-mode.md +41 -0
  592. package/src/prompts/tools/find.md +26 -0
  593. package/src/prompts/tools/gemini-image.md +7 -0
  594. package/src/prompts/tools/grep.md +29 -0
  595. package/src/prompts/tools/hashline.md +131 -0
  596. package/src/prompts/tools/inspect-image-system.md +20 -0
  597. package/src/prompts/tools/inspect-image.md +32 -0
  598. package/src/prompts/tools/lsp.md +33 -0
  599. package/src/prompts/tools/patch.md +77 -0
  600. package/src/prompts/tools/poll.md +5 -0
  601. package/src/prompts/tools/python.md +60 -0
  602. package/src/prompts/tools/read-chunk.md +35 -0
  603. package/src/prompts/tools/read.md +63 -0
  604. package/src/prompts/tools/render-mermaid.md +9 -0
  605. package/src/prompts/tools/replace.md +36 -0
  606. package/src/prompts/tools/resolve.md +8 -0
  607. package/src/prompts/tools/rewind.md +13 -0
  608. package/src/prompts/tools/search-tool-bm25.md +34 -0
  609. package/src/prompts/tools/ssh.md +44 -0
  610. package/src/prompts/tools/task-summary.md +28 -0
  611. package/src/prompts/tools/task.md +131 -0
  612. package/src/prompts/tools/todo-write.md +89 -0
  613. package/src/prompts/tools/vim.md +98 -0
  614. package/src/prompts/tools/web-search.md +10 -0
  615. package/src/prompts/tools/write.md +14 -0
  616. package/src/prompts/tools/xcsh-api.md +283 -0
  617. package/src/resource-management/index.ts +42 -0
  618. package/src/sdk.ts +2123 -0
  619. package/src/secrets/index.ts +146 -0
  620. package/src/secrets/obfuscator.ts +290 -0
  621. package/src/secrets/regex.ts +21 -0
  622. package/src/services/context-env.ts +102 -0
  623. package/src/services/xcsh-api-client.ts +230 -0
  624. package/src/services/xcsh-context-command.ts +803 -0
  625. package/src/services/xcsh-context-display.ts +49 -0
  626. package/src/services/xcsh-context-indicators.ts +17 -0
  627. package/src/services/xcsh-context-segment.ts +39 -0
  628. package/src/services/xcsh-context.ts +1410 -0
  629. package/src/services/xcsh-env.ts +107 -0
  630. package/src/services/xcsh-knowledge.ts +177 -0
  631. package/src/services/xcsh-table.ts +164 -0
  632. package/src/session/agent-session.ts +6690 -0
  633. package/src/session/agent-storage.ts +451 -0
  634. package/src/session/artifacts.ts +132 -0
  635. package/src/session/auth-storage.ts +17 -0
  636. package/src/session/blob-store.ts +135 -0
  637. package/src/session/compaction/branch-summarization.ts +322 -0
  638. package/src/session/compaction/compaction.ts +1388 -0
  639. package/src/session/compaction/index.ts +7 -0
  640. package/src/session/compaction/pruning.ts +91 -0
  641. package/src/session/compaction/utils.ts +184 -0
  642. package/src/session/history-storage.ts +253 -0
  643. package/src/session/messages.ts +597 -0
  644. package/src/session/session-dump-format.ts +203 -0
  645. package/src/session/session-manager.ts +2816 -0
  646. package/src/session/session-storage.ts +370 -0
  647. package/src/session/streaming-output.ts +770 -0
  648. package/src/session/tool-choice-queue.ts +213 -0
  649. package/src/slash-commands/builtin-registry.ts +1614 -0
  650. package/src/slash-commands/export-command.ts +178 -0
  651. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  652. package/src/slash-commands/resource-commands.ts +250 -0
  653. package/src/ssh/config-writer.ts +183 -0
  654. package/src/ssh/connection-manager.ts +445 -0
  655. package/src/ssh/ssh-executor.ts +131 -0
  656. package/src/ssh/sshfs-mount.ts +137 -0
  657. package/src/ssh/utils.ts +8 -0
  658. package/src/stt/downloader.ts +71 -0
  659. package/src/stt/index.ts +3 -0
  660. package/src/stt/recorder.ts +351 -0
  661. package/src/stt/setup.ts +52 -0
  662. package/src/stt/stt-controller.ts +160 -0
  663. package/src/stt/transcribe.py +70 -0
  664. package/src/stt/transcriber.ts +91 -0
  665. package/src/system-prompt.ts +688 -0
  666. package/src/task/agents.ts +166 -0
  667. package/src/task/commands.ts +131 -0
  668. package/src/task/discovery.ts +126 -0
  669. package/src/task/executor.ts +1324 -0
  670. package/src/task/index.ts +1197 -0
  671. package/src/task/isolation-backend.ts +72 -0
  672. package/src/task/name-generator.ts +1577 -0
  673. package/src/task/output-manager.ts +107 -0
  674. package/src/task/parallel.ts +116 -0
  675. package/src/task/render.ts +1035 -0
  676. package/src/task/subprocess-tool-registry.ts +88 -0
  677. package/src/task/template.ts +33 -0
  678. package/src/task/types.ts +230 -0
  679. package/src/task/worktree.ts +580 -0
  680. package/src/task/xcsh-command.ts +26 -0
  681. package/src/thinking.ts +87 -0
  682. package/src/tools/action-renderer.ts +198 -0
  683. package/src/tools/archive-reader.ts +315 -0
  684. package/src/tools/ask.ts +791 -0
  685. package/src/tools/ast-edit.ts +492 -0
  686. package/src/tools/ast-grep.ts +453 -0
  687. package/src/tools/auto-generated-guard.ts +305 -0
  688. package/src/tools/bash-interactive.ts +383 -0
  689. package/src/tools/bash-interceptor.ts +67 -0
  690. package/src/tools/bash-normalize.ts +107 -0
  691. package/src/tools/bash-skill-urls.ts +246 -0
  692. package/src/tools/bash.ts +878 -0
  693. package/src/tools/browser-renderer.ts +129 -0
  694. package/src/tools/browser.ts +1366 -0
  695. package/src/tools/calculator.ts +543 -0
  696. package/src/tools/cancel-job.ts +100 -0
  697. package/src/tools/catalog-workflow-runner.ts +718 -0
  698. package/src/tools/checkpoint.ts +133 -0
  699. package/src/tools/context.ts +39 -0
  700. package/src/tools/debug.ts +1004 -0
  701. package/src/tools/display-image-renderer.ts +96 -0
  702. package/src/tools/display-image.ts +109 -0
  703. package/src/tools/exit-plan-mode.ts +101 -0
  704. package/src/tools/fetch.ts +1462 -0
  705. package/src/tools/find.ts +437 -0
  706. package/src/tools/fs-cache-invalidation.ts +33 -0
  707. package/src/tools/gemini-image.ts +1004 -0
  708. package/src/tools/grep.ts +705 -0
  709. package/src/tools/index.ts +442 -0
  710. package/src/tools/inspect-image-renderer.ts +101 -0
  711. package/src/tools/inspect-image.ts +176 -0
  712. package/src/tools/json-tree.ts +266 -0
  713. package/src/tools/jtd-to-json-schema.ts +199 -0
  714. package/src/tools/jtd-to-typescript.ts +136 -0
  715. package/src/tools/jtd-utils.ts +102 -0
  716. package/src/tools/list-limit.ts +40 -0
  717. package/src/tools/mermaid-renderer.ts +127 -0
  718. package/src/tools/notebook.ts +288 -0
  719. package/src/tools/output-meta.ts +588 -0
  720. package/src/tools/path-utils.ts +560 -0
  721. package/src/tools/plan-mode-guard.ts +41 -0
  722. package/src/tools/poll-tool.ts +177 -0
  723. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  724. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  725. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  726. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  727. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  728. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  729. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  730. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  731. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  732. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  733. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  734. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  735. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  736. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  737. package/src/tools/python.ts +1184 -0
  738. package/src/tools/read.ts +1591 -0
  739. package/src/tools/render-mermaid.ts +93 -0
  740. package/src/tools/render-utils.ts +677 -0
  741. package/src/tools/renderers.ts +82 -0
  742. package/src/tools/report-tool-issue.ts +80 -0
  743. package/src/tools/resolve.ts +196 -0
  744. package/src/tools/review.ts +238 -0
  745. package/src/tools/search-tool-bm25.ts +288 -0
  746. package/src/tools/sqlite-reader.ts +623 -0
  747. package/src/tools/ssh.ts +322 -0
  748. package/src/tools/submit-result.ts +229 -0
  749. package/src/tools/todo-render.ts +33 -0
  750. package/src/tools/todo-write.ts +440 -0
  751. package/src/tools/tool-errors.ts +62 -0
  752. package/src/tools/tool-result.ts +93 -0
  753. package/src/tools/tool-timeouts.ts +30 -0
  754. package/src/tools/vim.ts +981 -0
  755. package/src/tools/write.ts +671 -0
  756. package/src/tools/xcsh-api-renderer.ts +402 -0
  757. package/src/tools/xcsh-api.ts +887 -0
  758. package/src/tui/code-cell.ts +101 -0
  759. package/src/tui/file-list.ts +47 -0
  760. package/src/tui/index.ts +11 -0
  761. package/src/tui/output-block.ts +175 -0
  762. package/src/tui/status-line.ts +39 -0
  763. package/src/tui/tree-list.ts +84 -0
  764. package/src/tui/types.ts +15 -0
  765. package/src/tui/utils.ts +103 -0
  766. package/src/utils/changelog.ts +98 -0
  767. package/src/utils/clipboard.ts +80 -0
  768. package/src/utils/command-args.ts +76 -0
  769. package/src/utils/commit-message-generator.ts +137 -0
  770. package/src/utils/edit-mode.ts +50 -0
  771. package/src/utils/event-bus.ts +33 -0
  772. package/src/utils/external-editor.ts +65 -0
  773. package/src/utils/file-display-mode.ts +37 -0
  774. package/src/utils/file-mentions.ts +376 -0
  775. package/src/utils/fuzzy.ts +108 -0
  776. package/src/utils/git.ts +1495 -0
  777. package/src/utils/gitstatus.ts +140 -0
  778. package/src/utils/image-convert.ts +27 -0
  779. package/src/utils/image-loading.ts +98 -0
  780. package/src/utils/image-passthrough.ts +118 -0
  781. package/src/utils/image-resize.ts +272 -0
  782. package/src/utils/image-viewer.ts +17 -0
  783. package/src/utils/lang-from-path.ts +239 -0
  784. package/src/utils/markit.ts +81 -0
  785. package/src/utils/open.ts +20 -0
  786. package/src/utils/session-color.ts +55 -0
  787. package/src/utils/shell-snapshot.ts +188 -0
  788. package/src/utils/title-generator.ts +193 -0
  789. package/src/utils/tool-choice.ts +28 -0
  790. package/src/utils/tools-manager.ts +356 -0
  791. package/src/vim/buffer.ts +309 -0
  792. package/src/vim/commands.ts +382 -0
  793. package/src/vim/engine.ts +2426 -0
  794. package/src/vim/parser.ts +151 -0
  795. package/src/vim/render.ts +252 -0
  796. package/src/vim/types.ts +197 -0
  797. package/src/web/kagi.ts +174 -0
  798. package/src/web/parallel.ts +346 -0
  799. package/src/web/scrapers/artifacthub.ts +207 -0
  800. package/src/web/scrapers/arxiv.ts +83 -0
  801. package/src/web/scrapers/aur.ts +162 -0
  802. package/src/web/scrapers/biorxiv.ts +133 -0
  803. package/src/web/scrapers/bluesky.ts +262 -0
  804. package/src/web/scrapers/brew.ts +172 -0
  805. package/src/web/scrapers/cheatsh.ts +68 -0
  806. package/src/web/scrapers/chocolatey.ts +196 -0
  807. package/src/web/scrapers/choosealicense.ts +95 -0
  808. package/src/web/scrapers/cisa-kev.ts +87 -0
  809. package/src/web/scrapers/clojars.ts +154 -0
  810. package/src/web/scrapers/coingecko.ts +177 -0
  811. package/src/web/scrapers/crates-io.ts +110 -0
  812. package/src/web/scrapers/crossref.ts +136 -0
  813. package/src/web/scrapers/devto.ts +147 -0
  814. package/src/web/scrapers/discogs.ts +306 -0
  815. package/src/web/scrapers/discourse.ts +197 -0
  816. package/src/web/scrapers/dockerhub.ts +138 -0
  817. package/src/web/scrapers/docs-rs.ts +653 -0
  818. package/src/web/scrapers/fdroid.ts +134 -0
  819. package/src/web/scrapers/firefox-addons.ts +191 -0
  820. package/src/web/scrapers/flathub.ts +223 -0
  821. package/src/web/scrapers/github-gist.ts +58 -0
  822. package/src/web/scrapers/github.ts +452 -0
  823. package/src/web/scrapers/gitlab.ts +401 -0
  824. package/src/web/scrapers/go-pkg.ts +266 -0
  825. package/src/web/scrapers/hackage.ts +140 -0
  826. package/src/web/scrapers/hackernews.ts +189 -0
  827. package/src/web/scrapers/hex.ts +105 -0
  828. package/src/web/scrapers/huggingface.ts +321 -0
  829. package/src/web/scrapers/iacr.ts +89 -0
  830. package/src/web/scrapers/index.ts +252 -0
  831. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  832. package/src/web/scrapers/lemmy.ts +203 -0
  833. package/src/web/scrapers/lobsters.ts +175 -0
  834. package/src/web/scrapers/mastodon.ts +292 -0
  835. package/src/web/scrapers/maven.ts +138 -0
  836. package/src/web/scrapers/mdn.ts +169 -0
  837. package/src/web/scrapers/metacpan.ts +222 -0
  838. package/src/web/scrapers/musicbrainz.ts +250 -0
  839. package/src/web/scrapers/npm.ts +98 -0
  840. package/src/web/scrapers/nuget.ts +183 -0
  841. package/src/web/scrapers/nvd.ts +222 -0
  842. package/src/web/scrapers/ollama.ts +239 -0
  843. package/src/web/scrapers/open-vsx.ts +106 -0
  844. package/src/web/scrapers/opencorporates.ts +292 -0
  845. package/src/web/scrapers/openlibrary.ts +336 -0
  846. package/src/web/scrapers/orcid.ts +286 -0
  847. package/src/web/scrapers/osv.ts +176 -0
  848. package/src/web/scrapers/packagist.ts +160 -0
  849. package/src/web/scrapers/pub-dev.ts +143 -0
  850. package/src/web/scrapers/pubmed.ts +211 -0
  851. package/src/web/scrapers/pypi.ts +112 -0
  852. package/src/web/scrapers/rawg.ts +110 -0
  853. package/src/web/scrapers/readthedocs.ts +120 -0
  854. package/src/web/scrapers/reddit.ts +95 -0
  855. package/src/web/scrapers/repology.ts +251 -0
  856. package/src/web/scrapers/rfc.ts +201 -0
  857. package/src/web/scrapers/rubygems.ts +103 -0
  858. package/src/web/scrapers/searchcode.ts +190 -0
  859. package/src/web/scrapers/sec-edgar.ts +261 -0
  860. package/src/web/scrapers/semantic-scholar.ts +171 -0
  861. package/src/web/scrapers/snapcraft.ts +187 -0
  862. package/src/web/scrapers/sourcegraph.ts +336 -0
  863. package/src/web/scrapers/spdx.ts +108 -0
  864. package/src/web/scrapers/spotify.ts +198 -0
  865. package/src/web/scrapers/stackoverflow.ts +120 -0
  866. package/src/web/scrapers/terraform.ts +277 -0
  867. package/src/web/scrapers/tldr.ts +47 -0
  868. package/src/web/scrapers/twitter.ts +93 -0
  869. package/src/web/scrapers/types.ts +267 -0
  870. package/src/web/scrapers/utils.ts +109 -0
  871. package/src/web/scrapers/vimeo.ts +133 -0
  872. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  873. package/src/web/scrapers/w3c.ts +156 -0
  874. package/src/web/scrapers/wikidata.ts +344 -0
  875. package/src/web/scrapers/wikipedia.ts +84 -0
  876. package/src/web/scrapers/youtube.ts +313 -0
  877. package/src/web/search/errors.ts +120 -0
  878. package/src/web/search/index.ts +346 -0
  879. package/src/web/search/params.ts +129 -0
  880. package/src/web/search/provider.ts +97 -0
  881. package/src/web/search/providers/anthropic.ts +339 -0
  882. package/src/web/search/providers/base.ts +35 -0
  883. package/src/web/search/providers/brave.ts +150 -0
  884. package/src/web/search/providers/codex.ts +402 -0
  885. package/src/web/search/providers/exa.ts +264 -0
  886. package/src/web/search/providers/firecrawl.ts +115 -0
  887. package/src/web/search/providers/gemini.ts +566 -0
  888. package/src/web/search/providers/jina.ts +99 -0
  889. package/src/web/search/providers/kagi.ts +70 -0
  890. package/src/web/search/providers/kimi.ts +157 -0
  891. package/src/web/search/providers/parallel.ts +63 -0
  892. package/src/web/search/providers/perplexity.ts +546 -0
  893. package/src/web/search/providers/synthetic.ts +113 -0
  894. package/src/web/search/providers/tavily.ts +162 -0
  895. package/src/web/search/providers/utils.ts +36 -0
  896. package/src/web/search/providers/zai.ts +311 -0
  897. package/src/web/search/render.ts +348 -0
  898. package/src/web/search/types.ts +438 -0
  899. 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
+ }