magus-cli 0.0.0-placeholder → 7.1.0

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 (330) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +725 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +545 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +524 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +248 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +340 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/open-file.test.ts +59 -0
  96. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  97. package/src/__tests__/plugin-checkout.test.ts +66 -0
  98. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  99. package/src/__tests__/plugin-contents.test.ts +371 -0
  100. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  101. package/src/__tests__/plugin-releases.test.ts +98 -0
  102. package/src/__tests__/plugin-requires.test.ts +187 -0
  103. package/src/__tests__/plugin-row-text.test.ts +345 -0
  104. package/src/__tests__/plugin-setup.test.ts +1016 -0
  105. package/src/__tests__/profile-adopt.test.ts +177 -0
  106. package/src/__tests__/profile-command.test.ts +110 -0
  107. package/src/__tests__/profile-materializer.test.ts +238 -0
  108. package/src/__tests__/profile-sync.test.ts +154 -0
  109. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  110. package/src/__tests__/resolve-executable.test.ts +42 -0
  111. package/src/__tests__/resolver.test.ts +351 -0
  112. package/src/__tests__/scope-action.test.ts +134 -0
  113. package/src/__tests__/scope-squares.test.tsx +164 -0
  114. package/src/__tests__/selection-scope.test.ts +100 -0
  115. package/src/__tests__/settings-file.test.ts +179 -0
  116. package/src/__tests__/shell-script-callers.test.ts +92 -0
  117. package/src/__tests__/skill-install-files.test.ts +136 -0
  118. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  119. package/src/__tests__/style-wrap.test.ts +220 -0
  120. package/src/__tests__/styles-manager.test.ts +1098 -0
  121. package/src/__tests__/styles-origins.test.ts +414 -0
  122. package/src/__tests__/styles-screen-state.test.ts +457 -0
  123. package/src/__tests__/styles-status-line.test.ts +72 -0
  124. package/src/__tests__/styles-sync.test.ts +452 -0
  125. package/src/__tests__/symlink-manager.test.ts +180 -0
  126. package/src/__tests__/tabbar-layout.test.ts +100 -0
  127. package/src/__tests__/terminology-filler.test.ts +214 -0
  128. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  129. package/src/__tests__/theme-env.test.ts +310 -0
  130. package/src/__tests__/theme-resolve.test.ts +380 -0
  131. package/src/__tests__/tool-probe.test.ts +1400 -0
  132. package/src/__tests__/toolchain.test.ts +232 -0
  133. package/src/__tests__/ui-version-writers.test.ts +88 -0
  134. package/src/__tests__/update-apply.test.ts +637 -0
  135. package/src/__tests__/update-plan.test.ts +1396 -0
  136. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  137. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  138. package/src/__tests__/version-check-refresh.test.ts +45 -0
  139. package/src/__tests__/version-snapshot.test.ts +212 -0
  140. package/src/__tests__/version-unknown-state.test.ts +403 -0
  141. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  142. package/src/cli/ansi.ts +528 -0
  143. package/src/cli/bootstrap.ts +134 -0
  144. package/src/cli/doctor.ts +218 -0
  145. package/src/cli/hook.ts +129 -0
  146. package/src/cli/install.ts +382 -0
  147. package/src/cli/live.ts +374 -0
  148. package/src/cli/models.ts +297 -0
  149. package/src/cli/profile.ts +197 -0
  150. package/src/cli/prompt.ts +32 -0
  151. package/src/cli/router.ts +264 -0
  152. package/src/cli/update-view.ts +717 -0
  153. package/src/cli/update.ts +600 -0
  154. package/src/cli/upgrade.ts +97 -0
  155. package/src/data/alias-flags.ts +301 -0
  156. package/src/data/cli-tools.ts +146 -0
  157. package/src/data/community-styles.ts +531 -0
  158. package/src/data/gitignore-defaults.ts +33 -0
  159. package/src/data/gitignore-reasons.ts +93 -0
  160. package/src/data/gitignore-templates.ts +25 -0
  161. package/src/data/marketplaces.ts +278 -0
  162. package/src/data/mcp-servers.ts +51 -0
  163. package/src/data/models-presets.ts +329 -0
  164. package/src/data/predefined-profiles.ts +252 -0
  165. package/src/data/settings-catalog.ts +714 -0
  166. package/src/data/skill-repos.ts +91 -0
  167. package/src/data/styles/asd-ste100.md +38 -0
  168. package/src/data/styles/calibrated.md +24 -0
  169. package/src/data/styles/direct.md +23 -0
  170. package/src/data/styles/evidence-first.md +22 -0
  171. package/src/data/styles/explanatory.md +23 -0
  172. package/src/data/styles/index.ts +57 -0
  173. package/src/data/styles/no-slop.md +32 -0
  174. package/src/data/styles/plain-language.md +23 -0
  175. package/src/data/styles/structured.md +30 -0
  176. package/src/data/styles/terminology.md +28 -0
  177. package/src/data/styles/terse.md +20 -0
  178. package/src/main.tsx +110 -0
  179. package/src/markdown.d.ts +13 -0
  180. package/src/opentui.d.ts +203 -0
  181. package/src/services/active-closure.ts +117 -0
  182. package/src/services/alias-settings.ts +60 -0
  183. package/src/services/alias-shell-writer.ts +1244 -0
  184. package/src/services/alias-store.ts +172 -0
  185. package/src/services/binary-signing.ts +78 -0
  186. package/src/services/catalog-cache-store.ts +316 -0
  187. package/src/services/claude-cli.ts +304 -0
  188. package/src/services/claude-runner.ts +31 -0
  189. package/src/services/claude-settings.ts +1564 -0
  190. package/src/services/cli-tool-commands.ts +133 -0
  191. package/src/services/community-fetcher.ts +90 -0
  192. package/src/services/community-styles.ts +1194 -0
  193. package/src/services/content-drift.ts +208 -0
  194. package/src/services/conventions-manager.ts +945 -0
  195. package/src/services/doctor-bins.ts +63 -0
  196. package/src/services/doctor.ts +570 -0
  197. package/src/services/dotenv.ts +181 -0
  198. package/src/services/git-worktree.ts +129 -0
  199. package/src/services/github-budget.ts +274 -0
  200. package/src/services/gitignore-check.ts +63 -0
  201. package/src/services/gitignore-detector.ts +167 -0
  202. package/src/services/gitignore-fixer.ts +280 -0
  203. package/src/services/gitignore-resolver.ts +208 -0
  204. package/src/services/gitignore-service.ts +167 -0
  205. package/src/services/hook-registration.ts +218 -0
  206. package/src/services/install-plan.ts +107 -0
  207. package/src/services/local-marketplace.ts +657 -0
  208. package/src/services/manifest.ts +250 -0
  209. package/src/services/marketplace-catalog-git.ts +173 -0
  210. package/src/services/marketplace-catalog.ts +129 -0
  211. package/src/services/marketplace-fetcher.ts +400 -0
  212. package/src/services/marketplace-refresh.ts +245 -0
  213. package/src/services/marketplace-sync.ts +145 -0
  214. package/src/services/mate-availability.ts +133 -0
  215. package/src/services/mate-catalog.ts +265 -0
  216. package/src/services/mcp-registry.ts +171 -0
  217. package/src/services/models-core.ts +969 -0
  218. package/src/services/models-manager.ts +606 -0
  219. package/src/services/plugin-checkout.ts +186 -0
  220. package/src/services/plugin-manager.ts +847 -0
  221. package/src/services/plugin-mcp-config.ts +242 -0
  222. package/src/services/plugin-releases.ts +47 -0
  223. package/src/services/plugin-requires.ts +227 -0
  224. package/src/services/plugin-setup.ts +965 -0
  225. package/src/services/profile-adopt.ts +206 -0
  226. package/src/services/profile-materializer.ts +133 -0
  227. package/src/services/profile-sync.ts +158 -0
  228. package/src/services/profiles.ts +225 -0
  229. package/src/services/resolver.ts +377 -0
  230. package/src/services/settings-file.ts +69 -0
  231. package/src/services/settings-manager.ts +307 -0
  232. package/src/services/skills-manager.ts +1111 -0
  233. package/src/services/skillsmp-client.ts +193 -0
  234. package/src/services/styles-manager.ts +1294 -0
  235. package/src/services/symlink-manager.ts +189 -0
  236. package/src/services/terminology-filler.ts +266 -0
  237. package/src/services/tool-probe.ts +1637 -0
  238. package/src/services/toolchain.ts +431 -0
  239. package/src/services/update-engine.ts +691 -0
  240. package/src/services/update-plan.ts +820 -0
  241. package/src/services/version-check.ts +127 -0
  242. package/src/services/version-snapshot.ts +189 -0
  243. package/src/tui.tsx +112 -0
  244. package/src/types/bun.d.ts +27 -0
  245. package/src/types/gitignore.ts +68 -0
  246. package/src/types/index.ts +447 -0
  247. package/src/ui/App.tsx +535 -0
  248. package/src/ui/adapters/catalogNotice.ts +122 -0
  249. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  250. package/src/ui/adapters/modelsAdapter.ts +213 -0
  251. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  252. package/src/ui/adapters/settingsAdapter.ts +168 -0
  253. package/src/ui/adapters/skillsAdapter.ts +229 -0
  254. package/src/ui/adapters/stylesAdapter.ts +403 -0
  255. package/src/ui/components/CategoryHeader.tsx +64 -0
  256. package/src/ui/components/EmptyFilterState.tsx +32 -0
  257. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  258. package/src/ui/components/ScopeIndicator.tsx +63 -0
  259. package/src/ui/components/ScrollableList.tsx +95 -0
  260. package/src/ui/components/SearchInput.tsx +57 -0
  261. package/src/ui/components/StyledText.tsx +75 -0
  262. package/src/ui/components/TabBar.tsx +101 -0
  263. package/src/ui/components/layout/FooterHints.tsx +78 -0
  264. package/src/ui/components/layout/Panel.tsx +63 -0
  265. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  266. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  267. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  268. package/src/ui/components/layout/index.ts +9 -0
  269. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  270. package/src/ui/components/modals/InputModal.tsx +75 -0
  271. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  272. package/src/ui/components/modals/MessageModal.tsx +66 -0
  273. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  274. package/src/ui/components/modals/SelectModal.tsx +90 -0
  275. package/src/ui/components/modals/index.ts +6 -0
  276. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  277. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  278. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  279. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  280. package/src/ui/components/primitives/MetaText.tsx +40 -0
  281. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  282. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  283. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  284. package/src/ui/components/primitives/index.ts +9 -0
  285. package/src/ui/hooks/index.ts +2 -0
  286. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  287. package/src/ui/hooks/useKeyboard.ts +24 -0
  288. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  289. package/src/ui/registry.ts +33 -0
  290. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  291. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  292. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  293. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  294. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  295. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  296. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  297. package/src/ui/renderers/pluginRowText.ts +70 -0
  298. package/src/ui/renderers/pluginStatusText.ts +56 -0
  299. package/src/ui/renderers/profileRenderers.tsx +406 -0
  300. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  301. package/src/ui/renderers/skillRenderers.tsx +479 -0
  302. package/src/ui/renderers/styleRenderers.tsx +813 -0
  303. package/src/ui/screens/AliasScreen.tsx +1558 -0
  304. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  305. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  306. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  307. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  308. package/src/ui/screens/McpScreen.tsx +256 -0
  309. package/src/ui/screens/ModelsScreen.tsx +542 -0
  310. package/src/ui/screens/PluginsScreen.tsx +1423 -0
  311. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  312. package/src/ui/screens/SkillsScreen.tsx +853 -0
  313. package/src/ui/screens/StylesScreen.tsx +1064 -0
  314. package/src/ui/screens/index.ts +11 -0
  315. package/src/ui/state/AppContext.tsx +233 -0
  316. package/src/ui/state/DimensionsContext.tsx +154 -0
  317. package/src/ui/state/reducer.ts +810 -0
  318. package/src/ui/state/types.ts +503 -0
  319. package/src/ui/theme-mode.ts +440 -0
  320. package/src/ui/theme-resolve.ts +147 -0
  321. package/src/ui/theme.ts +175 -0
  322. package/src/utils/clipboard.ts +58 -0
  323. package/src/utils/command-utils.ts +149 -0
  324. package/src/utils/config-dir.ts +45 -0
  325. package/src/utils/file-locking.ts +146 -0
  326. package/src/utils/fuzzy-search.ts +138 -0
  327. package/src/utils/go-module.ts +36 -0
  328. package/src/utils/open-file.ts +84 -0
  329. package/src/utils/run.ts +67 -0
  330. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,366 @@
1
+ import React, { useEffect, useCallback, useState, useRef } from "react";
2
+ import { ScreenLayout } from "../components/layout/index.js";
3
+ import { useKeyboard } from "../hooks/useKeyboard.js";
4
+ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
5
+ import { useDimensions } from "../state/DimensionsContext.js";
6
+
7
+ import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
8
+ import { formatDate, searchMcpServers } from "../../services/mcp-registry.js";
9
+ import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
10
+ import { ScrollableList } from "../components/ScrollableList.js";
11
+ import { theme } from "../theme.js";
12
+
13
+ /**
14
+ * Deduplicate servers by name, keeping only the latest version.
15
+ * Uses version string comparison, falling back to published_at date.
16
+ */
17
+ function deduplicateServers(servers: McpRegistryServer[]): McpRegistryServer[] {
18
+ const serverMap = new Map<string, McpRegistryServer>();
19
+
20
+ for (const server of servers) {
21
+ const existing = serverMap.get(server.name);
22
+ if (!existing) {
23
+ serverMap.set(server.name, server);
24
+ continue;
25
+ }
26
+
27
+ // Compare versions - keep the newer one
28
+ const isNewer = compareVersions(server, existing) > 0;
29
+ if (isNewer) {
30
+ serverMap.set(server.name, server);
31
+ }
32
+ }
33
+
34
+ return Array.from(serverMap.values());
35
+ }
36
+
37
+ /**
38
+ * Compare two servers by version. Returns:
39
+ * > 0 if a is newer
40
+ * < 0 if b is newer
41
+ * 0 if equal or cannot determine
42
+ */
43
+ function compareVersions(a: McpRegistryServer, b: McpRegistryServer): number {
44
+ // Try semver comparison first
45
+ if (a.version && b.version) {
46
+ const aParts = a.version.split(".").map((n) => Number.parseInt(n, 10) || 0);
47
+ const bParts = b.version.split(".").map((n) => Number.parseInt(n, 10) || 0);
48
+
49
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
50
+ const aVal = aParts[i] || 0;
51
+ const bVal = bParts[i] || 0;
52
+ if (aVal !== bVal) {
53
+ return aVal - bVal;
54
+ }
55
+ }
56
+ }
57
+
58
+ // Fall back to published_at date
59
+ if (a.published_at && b.published_at) {
60
+ return (
61
+ new Date(a.published_at).getTime() - new Date(b.published_at).getTime()
62
+ );
63
+ }
64
+
65
+ // If one has a date and other doesn't, prefer the one with date
66
+ if (a.published_at && !b.published_at) return 1;
67
+ if (!a.published_at && b.published_at) return -1;
68
+
69
+ return 0;
70
+ }
71
+
72
+ export function McpRegistryScreen() {
73
+ const { state, dispatch } = useApp();
74
+ const { mcpRegistry } = state;
75
+ const modal = useModal();
76
+ const { navigateToScreen } = useNavigation();
77
+ const dimensions = useDimensions();
78
+
79
+ const [servers, setServers] = useState<McpRegistryServer[]>([]);
80
+ const [isLoading, setIsLoading] = useState(true);
81
+ const [error, setError] = useState<string | null>(null);
82
+ const [searchQuery, setSearchQuery] = useState(mcpRegistry.searchQuery || "");
83
+
84
+ const isSearchActive =
85
+ state.isSearching &&
86
+ state.currentRoute.screen === "mcp-registry" &&
87
+ !state.modal;
88
+
89
+ const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
90
+
91
+ // Load servers
92
+ const loadServers = useCallback(async (query: string) => {
93
+ setIsLoading(true);
94
+ setError(null);
95
+ try {
96
+ const response = await searchMcpServers({ query, limit: 50 });
97
+ if (!response || !Array.isArray(response.servers)) {
98
+ throw new Error("Invalid response from MCP Registry API");
99
+ }
100
+ // Deduplicate to show only latest version of each server
101
+ const deduplicated = deduplicateServers(response.servers);
102
+ setServers(deduplicated);
103
+ } catch (err) {
104
+ setError(err instanceof Error ? err.message : "Failed to load servers");
105
+ setServers([]);
106
+ }
107
+ setIsLoading(false);
108
+ }, []);
109
+
110
+ // Debounced search
111
+ const debouncedSearch = useCallback(
112
+ (query: string) => {
113
+ if (searchTimeoutRef.current) {
114
+ clearTimeout(searchTimeoutRef.current);
115
+ }
116
+ searchTimeoutRef.current = setTimeout(() => {
117
+ loadServers(query);
118
+ }, 300);
119
+ },
120
+ [loadServers],
121
+ );
122
+
123
+ // Initial load runs once with the query the screen opened with; later edits
124
+ // go through debouncedSearch, so the live query is deliberately not a dep.
125
+ const initialQueryRef = useRef(searchQuery);
126
+ useEffect(() => {
127
+ loadServers(initialQueryRef.current);
128
+ return () => {
129
+ if (searchTimeoutRef.current) {
130
+ clearTimeout(searchTimeoutRef.current);
131
+ }
132
+ };
133
+ }, [loadServers]);
134
+
135
+ // Keyboard handling
136
+ useKeyboard((event) => {
137
+ // Handle search mode
138
+ if (isSearchActive) {
139
+ if (event.name === "escape") {
140
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
141
+ } else if (event.name === "enter") {
142
+ // Exit search mode and stay on list for navigation
143
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
144
+ } else if (event.name === "up") {
145
+ // Allow navigation while searching
146
+ const newIndex = Math.max(0, mcpRegistry.selectedIndex - 1);
147
+ dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
148
+ } else if (event.name === "down") {
149
+ // Allow navigation while searching
150
+ const newIndex = Math.min(
151
+ Math.max(0, servers.length - 1),
152
+ mcpRegistry.selectedIndex + 1,
153
+ );
154
+ dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
155
+ } else if (event.name === "backspace" || event.name === "delete") {
156
+ const newQuery = searchQuery.slice(0, -1);
157
+ setSearchQuery(newQuery);
158
+ dispatch({ type: "MCPREGISTRY_SEARCH", query: newQuery });
159
+ debouncedSearch(newQuery);
160
+ } else if (event.name.length === 1 && !event.ctrl && !event.meta) {
161
+ const newQuery = searchQuery + event.name;
162
+ setSearchQuery(newQuery);
163
+ dispatch({ type: "MCPREGISTRY_SEARCH", query: newQuery });
164
+ debouncedSearch(newQuery);
165
+ }
166
+ return;
167
+ }
168
+
169
+ if (state.modal) return;
170
+
171
+ // Start search with /
172
+ if (event.name === "/") {
173
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
174
+ return;
175
+ }
176
+
177
+ // Navigation
178
+ if (event.name === "up" || event.name === "k") {
179
+ const newIndex = Math.max(0, mcpRegistry.selectedIndex - 1);
180
+ dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
181
+ } else if (event.name === "down" || event.name === "j") {
182
+ const newIndex = Math.min(
183
+ Math.max(0, servers.length - 1),
184
+ mcpRegistry.selectedIndex + 1,
185
+ );
186
+ dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
187
+ } else if (event.name === "l") {
188
+ navigateToScreen("mcp");
189
+ } else if (event.name === "r" && event.shift) {
190
+ // `name` is always the unshifted key, so `=== "R"` never matched and
191
+ // refresh silently did nothing.
192
+ loadServers(searchQuery);
193
+ } else if (event.name === "enter") {
194
+ handleInstall();
195
+ }
196
+ });
197
+
198
+ const handleInstall = async () => {
199
+ const server = servers[mcpRegistry.selectedIndex];
200
+ if (!server) return;
201
+
202
+ const config: McpServerConfig = {
203
+ type: "http",
204
+ url: server.url,
205
+ };
206
+
207
+ modal.loading(`Installing ${server.name}...`);
208
+ try {
209
+ await setAllowMcp(true, state.projectPath);
210
+ await addMcpServer(server.name, config, state.projectPath);
211
+ modal.hideModal();
212
+ await modal.message(
213
+ "Installed",
214
+ `${server.name} has been configured.\n\nRestart Claude Code to activate.`,
215
+ "success",
216
+ );
217
+ } catch (error) {
218
+ modal.hideModal();
219
+ await modal.message("Error", `Failed to install: ${error}`, "error");
220
+ }
221
+ };
222
+
223
+ // Get selected server
224
+ const selectedServer = servers[mcpRegistry.selectedIndex];
225
+
226
+ const renderDetail = () => {
227
+ if (isLoading) {
228
+ return <text fg={theme.colors.muted}>Loading...</text>;
229
+ }
230
+
231
+ if (error) {
232
+ return <text fg={theme.colors.danger}>Error: {error}</text>;
233
+ }
234
+
235
+ if (!selectedServer) {
236
+ return (
237
+ <text fg={theme.colors.muted}>Select a server to see details</text>
238
+ );
239
+ }
240
+
241
+ const dateDisplay = selectedServer.published_at
242
+ ? formatDate(selectedServer.published_at)
243
+ : "unknown";
244
+
245
+ const versionDisplay = selectedServer.version
246
+ ? `v${selectedServer.version}`
247
+ : "unknown";
248
+
249
+ return (
250
+ <box flexDirection="column">
251
+ <text fg={theme.colors.accent}>
252
+ <strong>{selectedServer.name}</strong>
253
+ </text>
254
+ <box marginTop={1}>
255
+ <text fg={theme.colors.text}>{selectedServer.short_description}</text>
256
+ </box>
257
+ <box marginTop={1}>
258
+ <text fg={theme.colors.text}>
259
+ <strong>Version: </strong>
260
+ </text>
261
+ <text fg={theme.colors.success}>{versionDisplay}</text>
262
+ </box>
263
+ <box>
264
+ <text fg={theme.colors.text}>
265
+ <strong>Published: </strong>
266
+ </text>
267
+ <text fg={theme.colors.info}>{dateDisplay}</text>
268
+ </box>
269
+ <box marginTop={1} flexDirection="column">
270
+ <text fg={theme.colors.text}>
271
+ <strong>URL:</strong>
272
+ </text>
273
+ <text fg={theme.colors.info}>{selectedServer.url}</text>
274
+ </box>
275
+ {selectedServer.source_code_url && (
276
+ <box marginTop={1} flexDirection="column">
277
+ <text fg={theme.colors.text}>
278
+ <strong>Source:</strong>
279
+ </text>
280
+ <text fg={theme.colors.muted}>
281
+ {selectedServer.source_code_url}
282
+ </text>
283
+ </box>
284
+ )}
285
+ <box marginTop={1}>
286
+ <text fg={theme.colors.success}>Press Enter to install</text>
287
+ </box>
288
+ </box>
289
+ );
290
+ };
291
+
292
+ const renderListItem = (
293
+ server: McpRegistryServer,
294
+ _idx: number,
295
+ isSelected: boolean,
296
+ ) => {
297
+ const version = server.version ? ` v${server.version}` : "";
298
+ return isSelected ? (
299
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
300
+ {" "}
301
+ {server.name}
302
+ {version}{" "}
303
+ </text>
304
+ ) : (
305
+ <text fg={theme.colors.text}>
306
+ <span>
307
+ <strong>{server.name}</strong>
308
+ </span>
309
+ <span fg={theme.colors.success}>{version}</span>
310
+ </text>
311
+ );
312
+ };
313
+
314
+ // Footer hints
315
+ const footerHints = isSearchActive
316
+ ? [
317
+ { keys: ["type"], label: "search" },
318
+ { keys: ["↑", "↓"], label: "nav" },
319
+ { keys: ["Enter"], label: "done" },
320
+ { keys: ["Esc"], label: "cancel" },
321
+ ]
322
+ : [
323
+ { keys: ["↑", "↓"], label: "nav" },
324
+ { keys: ["Enter"], label: "install" },
325
+ { keys: ["/"], label: "search" },
326
+ { keys: ["R"], label: "refresh" },
327
+ { keys: ["l"], label: "local" },
328
+ ];
329
+
330
+ // Status for search placeholder
331
+ const searchPlaceholder = `${servers.length} servers │ / to search`;
332
+
333
+ return (
334
+ <ScreenLayout
335
+ title="magus MCP Registry"
336
+ subtitle="Powered by MCP Registry"
337
+ currentScreen="mcp-registry"
338
+ search={{
339
+ isActive: isSearchActive,
340
+ query: searchQuery,
341
+ placeholder: searchPlaceholder,
342
+ }}
343
+ footerHints={footerHints}
344
+ listPanel={
345
+ isLoading ? (
346
+ <text fg={theme.colors.muted}>Loading...</text>
347
+ ) : error ? (
348
+ <text fg={theme.colors.danger}>Error: {error}</text>
349
+ ) : servers.length === 0 ? (
350
+ <text fg={theme.colors.muted}>No servers found</text>
351
+ ) : (
352
+ <ScrollableList
353
+ items={servers}
354
+ selectedIndex={mcpRegistry.selectedIndex}
355
+ renderItem={renderListItem}
356
+ maxHeight={dimensions.listPanelHeight}
357
+ getKey={(server) => server.name}
358
+ />
359
+ )
360
+ }
361
+ detailPanel={renderDetail()}
362
+ />
363
+ );
364
+ }
365
+
366
+ export default McpRegistryScreen;
@@ -0,0 +1,256 @@
1
+ import React, { useEffect, useCallback, useMemo } from "react";
2
+ import { ScreenLayout } from "../components/layout/index.js";
3
+ import { useKeyboard } from "../hooks/useKeyboard.js";
4
+ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
5
+ import { useDimensions } from "../state/DimensionsContext.js";
6
+
7
+ import {
8
+ categoryOrder,
9
+ getCategoryDisplayName,
10
+ getMcpServersByCategory,
11
+ } from "../../data/mcp-servers.js";
12
+ import {
13
+ addMcpServer,
14
+ getEnabledMcpServers,
15
+ getInstalledMcpServers,
16
+ removeMcpServer,
17
+ } from "../../services/claude-settings.js";
18
+ import { fetchCuratedMcpServers } from "../../services/mcp-registry.js";
19
+ import type { McpServer, McpServerConfig } from "../../types/index.js";
20
+ import { ScrollableList } from "../components/ScrollableList.js";
21
+ import {
22
+ type McpListItem,
23
+ renderMcpDetail,
24
+ renderMcpRow,
25
+ } from "../renderers/mcpRenderers.js";
26
+
27
+ export function McpScreen() {
28
+ const { state, dispatch } = useApp();
29
+ const { mcp } = state;
30
+ const modal = useModal();
31
+ const { navigateToScreen } = useNavigation();
32
+ const dimensions = useDimensions();
33
+
34
+ const fetchData = useCallback(async () => {
35
+ dispatch({ type: "MCP_DATA_LOADING" });
36
+ try {
37
+ const curatedServers = await fetchCuratedMcpServers(
38
+ AbortSignal.timeout(10_000),
39
+ );
40
+ const serversByCategory = getMcpServersByCategory(curatedServers);
41
+ const installedServers = await getInstalledMcpServers(state.projectPath);
42
+ const enabledServers = await getEnabledMcpServers(state.projectPath);
43
+
44
+ const servers: McpServer[] = [];
45
+ for (const category of categoryOrder) {
46
+ const categoryServers = serversByCategory[category];
47
+ if (categoryServers) {
48
+ servers.push(...categoryServers);
49
+ }
50
+ }
51
+
52
+ const installedAsBooleans: Record<string, boolean> = {};
53
+ for (const name of Object.keys(installedServers)) {
54
+ installedAsBooleans[name] = true;
55
+ }
56
+
57
+ dispatch({
58
+ type: "MCP_DATA_SUCCESS",
59
+ servers,
60
+ installedServers: { ...installedAsBooleans, ...enabledServers },
61
+ });
62
+ } catch (error) {
63
+ dispatch({
64
+ type: "MCP_DATA_ERROR",
65
+ error: error instanceof Error ? error : new Error(String(error)),
66
+ });
67
+ }
68
+ }, [dispatch, state.projectPath]);
69
+
70
+ useEffect(() => {
71
+ fetchData();
72
+ }, [fetchData]);
73
+
74
+ const allListItems = useMemo((): McpListItem[] => {
75
+ if (mcp.servers.status !== "success") return [];
76
+
77
+ const serversByCategory = getMcpServersByCategory(mcp.servers.data);
78
+ const items: McpListItem[] = [];
79
+
80
+ for (const category of categoryOrder) {
81
+ const servers = serversByCategory[category];
82
+ if (!servers || servers.length === 0) continue;
83
+
84
+ items.push({ kind: "category", label: getCategoryDisplayName(category) });
85
+
86
+ for (const server of servers) {
87
+ const isInstalled = mcp.installedServers[server.name] !== undefined;
88
+ items.push({ kind: "server", server, isInstalled });
89
+ }
90
+ }
91
+
92
+ return items;
93
+ }, [mcp.servers, mcp.installedServers]);
94
+
95
+ useKeyboard((event) => {
96
+ if (state.isSearching || state.modal) return;
97
+
98
+ if (event.name === "/") {
99
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
100
+ navigateToScreen("mcp-registry");
101
+ return;
102
+ }
103
+
104
+ if (event.name === "up" || event.name === "k") {
105
+ const newIndex = Math.max(0, mcp.selectedIndex - 1);
106
+ dispatch({ type: "MCP_SELECT", index: newIndex });
107
+ } else if (event.name === "down" || event.name === "j") {
108
+ const newIndex = Math.min(allListItems.length - 1, mcp.selectedIndex + 1);
109
+ dispatch({ type: "MCP_SELECT", index: newIndex });
110
+ } else if (event.name === "r") {
111
+ navigateToScreen("mcp-registry");
112
+ } else if (event.name === "enter") {
113
+ handleSelect();
114
+ }
115
+ });
116
+
117
+ const handleSelect = async () => {
118
+ const item = allListItems[mcp.selectedIndex];
119
+ if (!item || item.kind === "category") return;
120
+
121
+ const { server, isInstalled } = item;
122
+
123
+ if (isInstalled) {
124
+ const confirmed = await modal.confirm(
125
+ `Remove ${server.name}?`,
126
+ "This will remove the MCP server configuration.",
127
+ );
128
+ if (confirmed) {
129
+ modal.loading(`Removing ${server.name}...`);
130
+ try {
131
+ await removeMcpServer(server.name, state.projectPath);
132
+ modal.hideModal();
133
+ await modal.message(
134
+ "Removed",
135
+ `${server.name} has been removed.`,
136
+ "success",
137
+ );
138
+ fetchData();
139
+ } catch (error) {
140
+ modal.hideModal();
141
+ await modal.message("Error", `Failed to remove: ${error}`, "error");
142
+ }
143
+ }
144
+ } else {
145
+ await installMcpServer(server);
146
+ }
147
+ };
148
+
149
+ const installMcpServer = async (server: McpServer) => {
150
+ let config: McpServerConfig;
151
+
152
+ if (server.type === "http") {
153
+ if (!server.url)
154
+ throw new Error(`${server.name} is missing its HTTP URL`);
155
+ config = { type: "http", url: server.url };
156
+ } else {
157
+ if (!server.command) {
158
+ throw new Error(`${server.name} is missing its launch command`);
159
+ }
160
+ config = {
161
+ command: server.command,
162
+ args: server.args ? [...server.args] : undefined,
163
+ env: server.env ? { ...server.env } : undefined,
164
+ };
165
+ }
166
+
167
+ if (server.requiresConfig && server.configFields) {
168
+ for (const field of server.configFields) {
169
+ const envVarName = field.envVar || field.name;
170
+ const existingValue = process.env[envVarName];
171
+
172
+ if (existingValue) {
173
+ const useExisting = await modal.confirm(
174
+ `Use ${envVarName}?`,
175
+ `${envVarName} is set in your environment. Use the existing value?`,
176
+ );
177
+ if (useExisting) {
178
+ config.env = config.env || {};
179
+ config.env[envVarName] = `\${${envVarName}}`;
180
+ continue;
181
+ }
182
+ }
183
+
184
+ const value = await modal.input(
185
+ `Configure ${server.name}`,
186
+ `${field.label}${field.required ? " (required)" : ""}:`,
187
+ field.default,
188
+ );
189
+ if (value === null) return;
190
+
191
+ if (field.required && !value) {
192
+ await modal.message(
193
+ "Required Field",
194
+ `${field.label} is required.`,
195
+ "error",
196
+ );
197
+ return;
198
+ }
199
+
200
+ if (value) {
201
+ config.env = config.env || {};
202
+ config.env[envVarName] = value;
203
+ }
204
+ }
205
+ }
206
+
207
+ modal.loading(`Installing ${server.name}...`);
208
+ try {
209
+ await addMcpServer(server.name, config, state.projectPath);
210
+ modal.hideModal();
211
+ await modal.message(
212
+ "Installed",
213
+ `${server.name} has been configured.\n\nRestart Claude Code to activate.`,
214
+ "success",
215
+ );
216
+ fetchData();
217
+ } catch (error) {
218
+ modal.hideModal();
219
+ await modal.message("Error", `Failed to install: ${error}`, "error");
220
+ }
221
+ };
222
+
223
+ const selectedItem = allListItems[mcp.selectedIndex];
224
+ const isLoading = mcp.servers.status === "loading";
225
+
226
+ const installedCount = Object.keys(mcp.installedServers).length;
227
+ const enabledCount = Object.values(mcp.installedServers).filter(
228
+ (v) => v === true,
229
+ ).length;
230
+ const subtitle = `${enabledCount} enabled │ ${installedCount} configured │ / to search`;
231
+
232
+ return (
233
+ <ScreenLayout
234
+ title="magus MCP Servers"
235
+ subtitle={subtitle}
236
+ currentScreen="mcp"
237
+ footerHints={[
238
+ { keys: ["↑", "↓"], label: "nav" },
239
+ { keys: ["Enter"], label: "toggle" },
240
+ { keys: ["/"], label: "search" },
241
+ { keys: ["r"], label: "registry" },
242
+ ]}
243
+ listPanel={
244
+ <ScrollableList
245
+ items={allListItems}
246
+ selectedIndex={mcp.selectedIndex}
247
+ renderItem={renderMcpRow}
248
+ maxHeight={dimensions.listPanelHeight}
249
+ />
250
+ }
251
+ detailPanel={renderMcpDetail(selectedItem, isLoading)}
252
+ />
253
+ );
254
+ }
255
+
256
+ export default McpScreen;