claudeup 1.7.0 → 3.3.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 (302) hide show
  1. package/bin/claudeup.js +20 -2
  2. package/package.json +10 -19
  3. package/src/data/cli-tools.js +123 -0
  4. package/src/data/cli-tools.ts +140 -0
  5. package/{dist → src}/data/marketplaces.js +23 -24
  6. package/src/data/marketplaces.ts +95 -0
  7. package/src/data/mcp-servers.js +509 -0
  8. package/src/data/mcp-servers.ts +526 -0
  9. package/src/data/statuslines.js +159 -0
  10. package/src/data/statuslines.ts +188 -0
  11. package/src/index.js +4 -0
  12. package/src/index.ts +5 -0
  13. package/{dist → src}/main.js +46 -47
  14. package/src/main.tsx +145 -0
  15. package/src/opentui.d.ts +191 -0
  16. package/src/prerunner/index.js +87 -0
  17. package/src/prerunner/index.ts +124 -0
  18. package/{dist → src}/services/claude-runner.js +9 -10
  19. package/src/services/claude-runner.ts +31 -0
  20. package/{dist → src}/services/claude-settings.js +72 -33
  21. package/src/services/claude-settings.ts +934 -0
  22. package/src/services/local-marketplace.js +339 -0
  23. package/src/services/local-marketplace.ts +489 -0
  24. package/{dist → src}/services/mcp-registry.js +13 -14
  25. package/src/services/mcp-registry.ts +105 -0
  26. package/{dist → src}/services/plugin-manager.js +61 -13
  27. package/src/services/plugin-manager.ts +693 -0
  28. package/{dist → src}/services/plugin-mcp-config.js +19 -18
  29. package/src/services/plugin-mcp-config.ts +242 -0
  30. package/{dist → src}/services/update-cache.js +7 -8
  31. package/src/services/update-cache.ts +78 -0
  32. package/{dist → src}/services/version-check.js +15 -14
  33. package/src/services/version-check.ts +122 -0
  34. package/src/types/index.js +1 -0
  35. package/src/types/index.ts +141 -0
  36. package/src/ui/App.js +213 -0
  37. package/src/ui/App.tsx +359 -0
  38. package/src/ui/components/CategoryHeader.js +9 -0
  39. package/src/ui/components/CategoryHeader.tsx +41 -0
  40. package/{dist → src}/ui/components/ScrollableList.js +19 -6
  41. package/src/ui/components/ScrollableList.tsx +98 -0
  42. package/src/ui/components/SearchInput.js +19 -0
  43. package/src/ui/components/SearchInput.tsx +56 -0
  44. package/src/ui/components/StyledText.js +39 -0
  45. package/src/ui/components/StyledText.tsx +70 -0
  46. package/src/ui/components/TabBar.js +38 -0
  47. package/src/ui/components/TabBar.tsx +88 -0
  48. package/src/ui/components/layout/Panel.js +6 -0
  49. package/src/ui/components/layout/Panel.tsx +62 -0
  50. package/src/ui/components/layout/ProgressBar.js +14 -0
  51. package/src/ui/components/layout/ProgressBar.tsx +47 -0
  52. package/src/ui/components/layout/ScopeTabs.js +6 -0
  53. package/src/ui/components/layout/ScopeTabs.tsx +53 -0
  54. package/src/ui/components/layout/ScreenLayout.js +21 -0
  55. package/src/ui/components/layout/ScreenLayout.tsx +147 -0
  56. package/src/ui/components/layout/index.js +4 -0
  57. package/src/ui/components/layout/index.ts +4 -0
  58. package/src/ui/components/modals/ConfirmModal.js +14 -0
  59. package/src/ui/components/modals/ConfirmModal.tsx +59 -0
  60. package/src/ui/components/modals/InputModal.js +16 -0
  61. package/src/ui/components/modals/InputModal.tsx +68 -0
  62. package/src/ui/components/modals/LoadingModal.js +14 -0
  63. package/src/ui/components/modals/LoadingModal.tsx +40 -0
  64. package/src/ui/components/modals/MessageModal.js +16 -0
  65. package/src/ui/components/modals/MessageModal.tsx +64 -0
  66. package/src/ui/components/modals/ModalContainer.js +56 -0
  67. package/src/ui/components/modals/ModalContainer.tsx +104 -0
  68. package/src/ui/components/modals/SelectModal.js +26 -0
  69. package/src/ui/components/modals/SelectModal.tsx +82 -0
  70. package/src/ui/components/modals/index.js +6 -0
  71. package/src/ui/components/modals/index.ts +6 -0
  72. package/src/ui/hooks/index.js +3 -0
  73. package/src/ui/hooks/index.ts +3 -0
  74. package/{dist → src}/ui/hooks/useAsyncData.js +21 -22
  75. package/src/ui/hooks/useAsyncData.ts +127 -0
  76. package/src/ui/hooks/useKeyboard.js +13 -0
  77. package/src/ui/hooks/useKeyboard.ts +26 -0
  78. package/src/ui/hooks/useKeyboardHandler.js +39 -0
  79. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  80. package/{dist → src}/ui/screens/CliToolsScreen.js +60 -54
  81. package/src/ui/screens/CliToolsScreen.tsx +468 -0
  82. package/src/ui/screens/EnvVarsScreen.js +154 -0
  83. package/src/ui/screens/EnvVarsScreen.tsx +269 -0
  84. package/{dist → src}/ui/screens/McpRegistryScreen.js +56 -55
  85. package/src/ui/screens/McpRegistryScreen.tsx +331 -0
  86. package/{dist → src}/ui/screens/McpScreen.js +46 -47
  87. package/src/ui/screens/McpScreen.tsx +392 -0
  88. package/src/ui/screens/ModelSelectorScreen.js +292 -0
  89. package/src/ui/screens/ModelSelectorScreen.tsx +441 -0
  90. package/{dist → src}/ui/screens/PluginsScreen.js +305 -293
  91. package/src/ui/screens/PluginsScreen.tsx +1231 -0
  92. package/src/ui/screens/StatusLineScreen.js +200 -0
  93. package/src/ui/screens/StatusLineScreen.tsx +411 -0
  94. package/src/ui/screens/index.js +7 -0
  95. package/src/ui/screens/index.ts +7 -0
  96. package/src/ui/state/AnimationContext.js +34 -0
  97. package/src/ui/state/AnimationContext.tsx +76 -0
  98. package/{dist → src}/ui/state/AppContext.js +31 -32
  99. package/src/ui/state/AppContext.tsx +235 -0
  100. package/{dist → src}/ui/state/DimensionsContext.js +16 -17
  101. package/src/ui/state/DimensionsContext.tsx +144 -0
  102. package/{dist → src}/ui/state/reducer.js +89 -90
  103. package/src/ui/state/reducer.ts +467 -0
  104. package/src/ui/state/types.js +1 -0
  105. package/src/ui/state/types.ts +273 -0
  106. package/{dist → src}/utils/command-utils.js +3 -4
  107. package/src/utils/command-utils.ts +20 -0
  108. package/{dist → src}/utils/fuzzy-search.js +2 -3
  109. package/src/utils/fuzzy-search.ts +138 -0
  110. package/{dist → src}/utils/string-utils.js +6 -6
  111. package/src/utils/string-utils.ts +88 -0
  112. package/dist/data/cli-tools.d.ts +0 -13
  113. package/dist/data/cli-tools.d.ts.map +0 -1
  114. package/dist/data/cli-tools.js +0 -124
  115. package/dist/data/cli-tools.js.map +0 -1
  116. package/dist/data/marketplaces.d.ts +0 -6
  117. package/dist/data/marketplaces.d.ts.map +0 -1
  118. package/dist/data/marketplaces.js.map +0 -1
  119. package/dist/data/mcp-servers.d.ts +0 -8
  120. package/dist/data/mcp-servers.d.ts.map +0 -1
  121. package/dist/data/mcp-servers.js +0 -503
  122. package/dist/data/mcp-servers.js.map +0 -1
  123. package/dist/data/statuslines.d.ts +0 -10
  124. package/dist/data/statuslines.d.ts.map +0 -1
  125. package/dist/data/statuslines.js +0 -160
  126. package/dist/data/statuslines.js.map +0 -1
  127. package/dist/index.d.ts +0 -3
  128. package/dist/index.d.ts.map +0 -1
  129. package/dist/index.js +0 -90
  130. package/dist/index.js.map +0 -1
  131. package/dist/main.d.ts +0 -3
  132. package/dist/main.d.ts.map +0 -1
  133. package/dist/main.js.map +0 -1
  134. package/dist/prerunner/index.d.ts +0 -7
  135. package/dist/prerunner/index.d.ts.map +0 -1
  136. package/dist/prerunner/index.js +0 -64
  137. package/dist/prerunner/index.js.map +0 -1
  138. package/dist/services/claude-runner.d.ts +0 -7
  139. package/dist/services/claude-runner.d.ts.map +0 -1
  140. package/dist/services/claude-runner.js.map +0 -1
  141. package/dist/services/claude-settings.d.ts +0 -73
  142. package/dist/services/claude-settings.d.ts.map +0 -1
  143. package/dist/services/claude-settings.js.map +0 -1
  144. package/dist/services/local-marketplace.d.ts +0 -111
  145. package/dist/services/local-marketplace.d.ts.map +0 -1
  146. package/dist/services/local-marketplace.js +0 -599
  147. package/dist/services/local-marketplace.js.map +0 -1
  148. package/dist/services/mcp-registry.d.ts +0 -10
  149. package/dist/services/mcp-registry.d.ts.map +0 -1
  150. package/dist/services/mcp-registry.js.map +0 -1
  151. package/dist/services/plugin-manager.d.ts +0 -65
  152. package/dist/services/plugin-manager.d.ts.map +0 -1
  153. package/dist/services/plugin-manager.js.map +0 -1
  154. package/dist/services/plugin-mcp-config.d.ts +0 -52
  155. package/dist/services/plugin-mcp-config.d.ts.map +0 -1
  156. package/dist/services/plugin-mcp-config.js.map +0 -1
  157. package/dist/services/update-cache.d.ts +0 -16
  158. package/dist/services/update-cache.d.ts.map +0 -1
  159. package/dist/services/update-cache.js.map +0 -1
  160. package/dist/services/version-check.d.ts +0 -20
  161. package/dist/services/version-check.d.ts.map +0 -1
  162. package/dist/services/version-check.js.map +0 -1
  163. package/dist/types/index.d.ts +0 -105
  164. package/dist/types/index.d.ts.map +0 -1
  165. package/dist/types/index.js +0 -2
  166. package/dist/types/index.js.map +0 -1
  167. package/dist/ui/InkApp.d.ts +0 -5
  168. package/dist/ui/InkApp.d.ts.map +0 -1
  169. package/dist/ui/InkApp.js +0 -188
  170. package/dist/ui/InkApp.js.map +0 -1
  171. package/dist/ui/components/CategoryHeader.d.ts +0 -16
  172. package/dist/ui/components/CategoryHeader.d.ts.map +0 -1
  173. package/dist/ui/components/CategoryHeader.js +0 -11
  174. package/dist/ui/components/CategoryHeader.js.map +0 -1
  175. package/dist/ui/components/ScrollableList.d.ts +0 -16
  176. package/dist/ui/components/ScrollableList.d.ts.map +0 -1
  177. package/dist/ui/components/ScrollableList.js.map +0 -1
  178. package/dist/ui/components/SearchInput.d.ts +0 -18
  179. package/dist/ui/components/SearchInput.d.ts.map +0 -1
  180. package/dist/ui/components/SearchInput.js +0 -30
  181. package/dist/ui/components/SearchInput.js.map +0 -1
  182. package/dist/ui/components/TabBar.d.ts +0 -8
  183. package/dist/ui/components/TabBar.d.ts.map +0 -1
  184. package/dist/ui/components/TabBar.js +0 -18
  185. package/dist/ui/components/TabBar.js.map +0 -1
  186. package/dist/ui/components/layout/Footer.d.ts +0 -14
  187. package/dist/ui/components/layout/Footer.d.ts.map +0 -1
  188. package/dist/ui/components/layout/Footer.js +0 -23
  189. package/dist/ui/components/layout/Footer.js.map +0 -1
  190. package/dist/ui/components/layout/Header.d.ts +0 -4
  191. package/dist/ui/components/layout/Header.d.ts.map +0 -1
  192. package/dist/ui/components/layout/Header.js +0 -25
  193. package/dist/ui/components/layout/Header.js.map +0 -1
  194. package/dist/ui/components/layout/Panel.d.ts +0 -22
  195. package/dist/ui/components/layout/Panel.d.ts.map +0 -1
  196. package/dist/ui/components/layout/Panel.js +0 -8
  197. package/dist/ui/components/layout/Panel.js.map +0 -1
  198. package/dist/ui/components/layout/ProgressBar.d.ts +0 -12
  199. package/dist/ui/components/layout/ProgressBar.d.ts.map +0 -1
  200. package/dist/ui/components/layout/ProgressBar.js +0 -16
  201. package/dist/ui/components/layout/ProgressBar.js.map +0 -1
  202. package/dist/ui/components/layout/ScopeTabs.d.ts +0 -12
  203. package/dist/ui/components/layout/ScopeTabs.d.ts.map +0 -1
  204. package/dist/ui/components/layout/ScopeTabs.js +0 -8
  205. package/dist/ui/components/layout/ScopeTabs.js.map +0 -1
  206. package/dist/ui/components/layout/ScreenLayout.d.ts +0 -30
  207. package/dist/ui/components/layout/ScreenLayout.d.ts.map +0 -1
  208. package/dist/ui/components/layout/ScreenLayout.js +0 -23
  209. package/dist/ui/components/layout/ScreenLayout.js.map +0 -1
  210. package/dist/ui/components/layout/index.d.ts +0 -7
  211. package/dist/ui/components/layout/index.d.ts.map +0 -1
  212. package/dist/ui/components/layout/index.js +0 -7
  213. package/dist/ui/components/layout/index.js.map +0 -1
  214. package/dist/ui/components/modals/ConfirmModal.d.ts +0 -14
  215. package/dist/ui/components/modals/ConfirmModal.d.ts.map +0 -1
  216. package/dist/ui/components/modals/ConfirmModal.js +0 -15
  217. package/dist/ui/components/modals/ConfirmModal.js.map +0 -1
  218. package/dist/ui/components/modals/InputModal.d.ts +0 -16
  219. package/dist/ui/components/modals/InputModal.d.ts.map +0 -1
  220. package/dist/ui/components/modals/InputModal.js +0 -23
  221. package/dist/ui/components/modals/InputModal.js.map +0 -1
  222. package/dist/ui/components/modals/LoadingModal.d.ts +0 -8
  223. package/dist/ui/components/modals/LoadingModal.d.ts.map +0 -1
  224. package/dist/ui/components/modals/LoadingModal.js +0 -8
  225. package/dist/ui/components/modals/LoadingModal.js.map +0 -1
  226. package/dist/ui/components/modals/MessageModal.d.ts +0 -14
  227. package/dist/ui/components/modals/MessageModal.d.ts.map +0 -1
  228. package/dist/ui/components/modals/MessageModal.js +0 -17
  229. package/dist/ui/components/modals/MessageModal.js.map +0 -1
  230. package/dist/ui/components/modals/ModalContainer.d.ts +0 -7
  231. package/dist/ui/components/modals/ModalContainer.d.ts.map +0 -1
  232. package/dist/ui/components/modals/ModalContainer.js +0 -38
  233. package/dist/ui/components/modals/ModalContainer.js.map +0 -1
  234. package/dist/ui/components/modals/SelectModal.d.ts +0 -17
  235. package/dist/ui/components/modals/SelectModal.d.ts.map +0 -1
  236. package/dist/ui/components/modals/SelectModal.js +0 -33
  237. package/dist/ui/components/modals/SelectModal.js.map +0 -1
  238. package/dist/ui/components/modals/index.d.ts +0 -7
  239. package/dist/ui/components/modals/index.d.ts.map +0 -1
  240. package/dist/ui/components/modals/index.js +0 -7
  241. package/dist/ui/components/modals/index.js.map +0 -1
  242. package/dist/ui/hooks/index.d.ts +0 -3
  243. package/dist/ui/hooks/index.d.ts.map +0 -1
  244. package/dist/ui/hooks/index.js +0 -3
  245. package/dist/ui/hooks/index.js.map +0 -1
  246. package/dist/ui/hooks/useAsyncData.d.ts +0 -40
  247. package/dist/ui/hooks/useAsyncData.d.ts.map +0 -1
  248. package/dist/ui/hooks/useAsyncData.js.map +0 -1
  249. package/dist/ui/hooks/useKeyboardNavigation.d.ts +0 -27
  250. package/dist/ui/hooks/useKeyboardNavigation.d.ts.map +0 -1
  251. package/dist/ui/hooks/useKeyboardNavigation.js +0 -82
  252. package/dist/ui/hooks/useKeyboardNavigation.js.map +0 -1
  253. package/dist/ui/screens/CliToolsScreen.d.ts +0 -4
  254. package/dist/ui/screens/CliToolsScreen.d.ts.map +0 -1
  255. package/dist/ui/screens/CliToolsScreen.js.map +0 -1
  256. package/dist/ui/screens/EnvVarsScreen.d.ts +0 -4
  257. package/dist/ui/screens/EnvVarsScreen.d.ts.map +0 -1
  258. package/dist/ui/screens/EnvVarsScreen.js +0 -145
  259. package/dist/ui/screens/EnvVarsScreen.js.map +0 -1
  260. package/dist/ui/screens/McpRegistryScreen.d.ts +0 -4
  261. package/dist/ui/screens/McpRegistryScreen.d.ts.map +0 -1
  262. package/dist/ui/screens/McpRegistryScreen.js.map +0 -1
  263. package/dist/ui/screens/McpScreen.d.ts +0 -4
  264. package/dist/ui/screens/McpScreen.d.ts.map +0 -1
  265. package/dist/ui/screens/McpScreen.js.map +0 -1
  266. package/dist/ui/screens/ModelSelectorScreen.d.ts +0 -4
  267. package/dist/ui/screens/ModelSelectorScreen.d.ts.map +0 -1
  268. package/dist/ui/screens/ModelSelectorScreen.js +0 -143
  269. package/dist/ui/screens/ModelSelectorScreen.js.map +0 -1
  270. package/dist/ui/screens/PluginsScreen.d.ts +0 -4
  271. package/dist/ui/screens/PluginsScreen.d.ts.map +0 -1
  272. package/dist/ui/screens/PluginsScreen.js.map +0 -1
  273. package/dist/ui/screens/StatusLineScreen.d.ts +0 -4
  274. package/dist/ui/screens/StatusLineScreen.d.ts.map +0 -1
  275. package/dist/ui/screens/StatusLineScreen.js +0 -197
  276. package/dist/ui/screens/StatusLineScreen.js.map +0 -1
  277. package/dist/ui/screens/index.d.ts +0 -8
  278. package/dist/ui/screens/index.d.ts.map +0 -1
  279. package/dist/ui/screens/index.js +0 -8
  280. package/dist/ui/screens/index.js.map +0 -1
  281. package/dist/ui/state/AppContext.d.ts +0 -40
  282. package/dist/ui/state/AppContext.d.ts.map +0 -1
  283. package/dist/ui/state/AppContext.js.map +0 -1
  284. package/dist/ui/state/DimensionsContext.d.ts +0 -27
  285. package/dist/ui/state/DimensionsContext.d.ts.map +0 -1
  286. package/dist/ui/state/DimensionsContext.js.map +0 -1
  287. package/dist/ui/state/reducer.d.ts +0 -4
  288. package/dist/ui/state/reducer.d.ts.map +0 -1
  289. package/dist/ui/state/reducer.js.map +0 -1
  290. package/dist/ui/state/types.d.ts +0 -266
  291. package/dist/ui/state/types.d.ts.map +0 -1
  292. package/dist/ui/state/types.js +0 -2
  293. package/dist/ui/state/types.js.map +0 -1
  294. package/dist/utils/command-utils.d.ts +0 -8
  295. package/dist/utils/command-utils.d.ts.map +0 -1
  296. package/dist/utils/command-utils.js.map +0 -1
  297. package/dist/utils/fuzzy-search.d.ts +0 -33
  298. package/dist/utils/fuzzy-search.d.ts.map +0 -1
  299. package/dist/utils/fuzzy-search.js.map +0 -1
  300. package/dist/utils/string-utils.d.ts +0 -24
  301. package/dist/utils/string-utils.d.ts.map +0 -1
  302. package/dist/utils/string-utils.js.map +0 -1
@@ -0,0 +1,392 @@
1
+ import React, { useEffect, useCallback, useMemo } from "react";
2
+ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
3
+ import { useDimensions } from "../state/DimensionsContext.js";
4
+ import { useKeyboard } from "../hooks/useKeyboard.js";
5
+ import { ScreenLayout } from "../components/layout/index.js";
6
+ import { ScrollableList } from "../components/ScrollableList.js";
7
+ import {
8
+ getMcpServersByCategory,
9
+ getCategoryDisplayName,
10
+ categoryOrder,
11
+ } from "../../data/mcp-servers.js";
12
+ import {
13
+ addMcpServer,
14
+ removeMcpServer,
15
+ getInstalledMcpServers,
16
+ getEnabledMcpServers,
17
+ } from "../../services/claude-settings.js";
18
+ import type { McpServer, McpServerConfig } from "../../types/index.js";
19
+
20
+ interface ListItem {
21
+ label: string;
22
+ server?: McpServer;
23
+ isCategory?: boolean;
24
+ }
25
+
26
+ export function McpScreen() {
27
+ const { state, dispatch } = useApp();
28
+ const { mcp } = state;
29
+ const modal = useModal();
30
+ const { navigateToScreen } = useNavigation();
31
+ const dimensions = useDimensions();
32
+
33
+ // Fetch data
34
+ const fetchData = useCallback(async () => {
35
+ dispatch({ type: "MCP_DATA_LOADING" });
36
+ try {
37
+ const serversByCategory = getMcpServersByCategory();
38
+ const installedServers = await getInstalledMcpServers(state.projectPath);
39
+ const enabledServers = await getEnabledMcpServers(state.projectPath);
40
+
41
+ // Build flat list of all servers
42
+ const servers: McpServer[] = [];
43
+ for (const category of categoryOrder) {
44
+ const categoryServers = serversByCategory[category];
45
+ if (categoryServers) {
46
+ servers.push(...categoryServers);
47
+ }
48
+ }
49
+
50
+ // Convert McpServerConfig to boolean - server exists = installed
51
+ const installedAsBooleans: Record<string, boolean> = {};
52
+ for (const name of Object.keys(installedServers)) {
53
+ installedAsBooleans[name] = true;
54
+ }
55
+
56
+ dispatch({
57
+ type: "MCP_DATA_SUCCESS",
58
+ servers,
59
+ installedServers: { ...installedAsBooleans, ...enabledServers },
60
+ });
61
+ } catch (error) {
62
+ dispatch({
63
+ type: "MCP_DATA_ERROR",
64
+ error: error instanceof Error ? error : new Error(String(error)),
65
+ });
66
+ }
67
+ }, [dispatch, state.projectPath]);
68
+
69
+ useEffect(() => {
70
+ fetchData();
71
+ }, [fetchData]);
72
+
73
+ // Build list items with categories
74
+ const allListItems = useMemo((): ListItem[] => {
75
+ if (mcp.servers.status !== "success") return [];
76
+
77
+ const serversByCategory = getMcpServersByCategory();
78
+ const items: ListItem[] = [];
79
+
80
+ for (const category of categoryOrder) {
81
+ const servers = serversByCategory[category];
82
+ if (!servers || servers.length === 0) continue;
83
+
84
+ items.push({
85
+ label: getCategoryDisplayName(category),
86
+ isCategory: true,
87
+ });
88
+
89
+ for (const server of servers) {
90
+ const isInstalled = mcp.installedServers[server.name] !== undefined;
91
+ const isEnabled = mcp.installedServers[server.name] === true;
92
+
93
+ let status = "○";
94
+ if (isInstalled && isEnabled) {
95
+ status = "●";
96
+ } else if (isInstalled) {
97
+ status = "●";
98
+ }
99
+
100
+ const configTag = server.requiresConfig ? " *" : "";
101
+
102
+ items.push({
103
+ label: ` ${status} ${server.name}${configTag}`,
104
+ server,
105
+ });
106
+ }
107
+ }
108
+
109
+ return items;
110
+ }, [mcp.servers.status, mcp.installedServers]);
111
+
112
+ // Use all items - search goes to global registry, not local filter
113
+ const listItems = allListItems;
114
+
115
+ // Keyboard handling
116
+ useKeyboard((event) => {
117
+ if (state.isSearching || state.modal) return;
118
+
119
+ // Start search - navigate to registry with search mode active
120
+ if (event.name === "/") {
121
+ dispatch({ type: "SET_SEARCHING", isSearching: true });
122
+ navigateToScreen("mcp-registry");
123
+ return;
124
+ }
125
+
126
+ if (event.name === "up" || event.name === "k") {
127
+ const newIndex = Math.max(0, mcp.selectedIndex - 1);
128
+ dispatch({ type: "MCP_SELECT", index: newIndex });
129
+ } else if (event.name === "down" || event.name === "j") {
130
+ const newIndex = Math.min(listItems.length - 1, mcp.selectedIndex + 1);
131
+ dispatch({ type: "MCP_SELECT", index: newIndex });
132
+ } else if (event.name === "r") {
133
+ navigateToScreen("mcp-registry");
134
+ } else if (event.name === "enter") {
135
+ handleSelect();
136
+ }
137
+ });
138
+
139
+ const handleSelect = async () => {
140
+ const item = listItems[mcp.selectedIndex];
141
+ if (!item || item.isCategory || !item.server) return;
142
+
143
+ const server = item.server;
144
+ const isInstalled = mcp.installedServers[server.name] !== undefined;
145
+
146
+ if (isInstalled) {
147
+ const confirmed = await modal.confirm(
148
+ `Remove ${server.name}?`,
149
+ "This will remove the MCP server configuration.",
150
+ );
151
+ if (confirmed) {
152
+ modal.loading(`Removing ${server.name}...`);
153
+ try {
154
+ await removeMcpServer(server.name, state.projectPath);
155
+ modal.hideModal();
156
+ await modal.message(
157
+ "Removed",
158
+ `${server.name} has been removed.`,
159
+ "success",
160
+ );
161
+ fetchData();
162
+ } catch (error) {
163
+ modal.hideModal();
164
+ await modal.message("Error", `Failed to remove: ${error}`, "error");
165
+ }
166
+ }
167
+ } else {
168
+ // Install server
169
+ await installMcpServer(server);
170
+ }
171
+ };
172
+
173
+ const installMcpServer = async (server: McpServer) => {
174
+ let config: McpServerConfig;
175
+
176
+ if (server.type === "http") {
177
+ config = { type: "http", url: server.url! };
178
+ } else {
179
+ config = {
180
+ command: server.command!,
181
+ args: server.args ? [...server.args] : undefined,
182
+ env: server.env ? { ...server.env } : undefined,
183
+ };
184
+ }
185
+
186
+ // Handle configuration fields if required
187
+ if (server.requiresConfig && server.configFields) {
188
+ for (const field of server.configFields) {
189
+ const envVarName = field.envVar || field.name;
190
+ const existingValue = process.env[envVarName];
191
+
192
+ if (existingValue) {
193
+ const useExisting = await modal.confirm(
194
+ `Use ${envVarName}?`,
195
+ `${envVarName} is set in your environment. Use the existing value?`,
196
+ );
197
+ if (useExisting) {
198
+ config.env = config.env || {};
199
+ config.env[envVarName] = `\${${envVarName}}`;
200
+ continue;
201
+ }
202
+ }
203
+
204
+ const value = await modal.input(
205
+ `Configure ${server.name}`,
206
+ `${field.label}${field.required ? " (required)" : ""}:`,
207
+ field.default,
208
+ );
209
+
210
+ if (value === null) return; // User cancelled
211
+
212
+ if (field.required && !value) {
213
+ await modal.message(
214
+ "Required Field",
215
+ `${field.label} is required.`,
216
+ "error",
217
+ );
218
+ return;
219
+ }
220
+
221
+ if (value) {
222
+ config.env = config.env || {};
223
+ config.env[envVarName] = value;
224
+ }
225
+ }
226
+ }
227
+
228
+ modal.loading(`Installing ${server.name}...`);
229
+ try {
230
+ await addMcpServer(server.name, config, state.projectPath);
231
+ modal.hideModal();
232
+ await modal.message(
233
+ "Installed",
234
+ `${server.name} has been configured.\n\nRestart Claude Code to activate.`,
235
+ "success",
236
+ );
237
+ fetchData();
238
+ } catch (error) {
239
+ modal.hideModal();
240
+ await modal.message("Error", `Failed to install: ${error}`, "error");
241
+ }
242
+ };
243
+
244
+ // Get selected item
245
+ const selectedItem = listItems[mcp.selectedIndex];
246
+
247
+ const renderDetail = () => {
248
+ if (mcp.servers.status === "loading") {
249
+ return <text fg="gray">Loading MCP servers...</text>;
250
+ }
251
+
252
+ if (!selectedItem || selectedItem.isCategory || !selectedItem.server) {
253
+ return (
254
+ <box
255
+ flexDirection="column"
256
+ alignItems="center"
257
+ justifyContent="center"
258
+ flexGrow={1}
259
+ >
260
+ <text fg="gray">Select a server to see details</text>
261
+ </box>
262
+ );
263
+ }
264
+
265
+ const server = selectedItem.server;
266
+ const isInstalled = mcp.installedServers[server.name] !== undefined;
267
+ const isEnabled = mcp.installedServers[server.name] === true;
268
+
269
+ return (
270
+ <box flexDirection="column">
271
+ <box marginBottom={1}>
272
+ <text fg="cyan"><strong>⚡ {server.name}</strong></text>
273
+ {server.requiresConfig && <text fg="yellow"> ⚙</text>}
274
+ </box>
275
+
276
+ <text fg="gray">{server.description}</text>
277
+
278
+ <box marginTop={1} flexDirection="column">
279
+ <box>
280
+ <text fg="gray">Status </text>
281
+ {isInstalled && isEnabled ? (
282
+ <text fg="green">● Installed</text>
283
+ ) : isInstalled ? (
284
+ <text fg="yellow">● Disabled</text>
285
+ ) : (
286
+ <text fg="gray">○ Not installed</text>
287
+ )}
288
+ </box>
289
+ <box>
290
+ <text fg="gray">Type </text>
291
+ <text fg="white">
292
+ {server.type === "http" ? "HTTP" : "Command"}
293
+ </text>
294
+ </box>
295
+ {server.type === "http" ? (
296
+ <box>
297
+ <text fg="gray">URL </text>
298
+ <text fg="blue">{server.url}</text>
299
+ </box>
300
+ ) : (
301
+ <box>
302
+ <text fg="gray">Command </text>
303
+ <text fg="cyan">{server.command}</text>
304
+ </box>
305
+ )}
306
+ {server.requiresConfig && (
307
+ <box>
308
+ <text fg="gray">Config </text>
309
+ <text fg="yellow">
310
+ {server.configFields?.length || 0} fields required
311
+ </text>
312
+ </box>
313
+ )}
314
+ </box>
315
+
316
+ <box marginTop={2}>
317
+ {isInstalled ? (
318
+ <box>
319
+ <text bg="red" fg="white"> Enter </text>
320
+ <text fg="gray"> Remove server</text>
321
+ </box>
322
+ ) : (
323
+ <box>
324
+ <text bg="green" fg="black"> Enter </text>
325
+ <text fg="gray"> Install server</text>
326
+ </box>
327
+ )}
328
+ </box>
329
+ </box>
330
+ );
331
+ };
332
+
333
+ const renderListItem = (
334
+ item: ListItem,
335
+ _idx: number,
336
+ isSelected: boolean,
337
+ ) => {
338
+ // Category header
339
+ if (item.isCategory) {
340
+ return (
341
+ <text fg="magenta"><strong>▸ {item.label}</strong></text>
342
+ );
343
+ }
344
+
345
+ // Server item
346
+ const server = item.server;
347
+ const isInstalled =
348
+ server && mcp.installedServers[server.name] !== undefined;
349
+ const icon = isInstalled ? "●" : "○";
350
+ const iconColor = isInstalled ? "green" : "gray";
351
+
352
+ return isSelected ? (
353
+ <text bg="magenta" fg="white">
354
+ {" "}
355
+ {icon} {server?.name || ""}{" "}
356
+ </text>
357
+ ) : (
358
+ <text>
359
+ <span fg={iconColor}>{icon}</span>
360
+ <span fg="white"> {server?.name || ""}</span>
361
+ {server?.requiresConfig && <span fg="yellow"> ⚙</span>}
362
+ </text>
363
+ );
364
+ };
365
+
366
+ // Calculate status counts
367
+ const installedCount = Object.keys(mcp.installedServers).length;
368
+ const enabledCount = Object.values(mcp.installedServers).filter(
369
+ (v) => v === true,
370
+ ).length;
371
+ const subtitle = `${enabledCount} enabled │ ${installedCount} configured │ / to search`;
372
+
373
+ return (
374
+ <ScreenLayout
375
+ title="claudeup MCP Servers"
376
+ subtitle={subtitle}
377
+ currentScreen="mcp"
378
+ footerHints="↑↓:nav │ Enter:toggle │ /:search │ r:registry"
379
+ listPanel={
380
+ <ScrollableList
381
+ items={listItems}
382
+ selectedIndex={mcp.selectedIndex}
383
+ renderItem={renderListItem}
384
+ maxHeight={dimensions.listPanelHeight}
385
+ />
386
+ }
387
+ detailPanel={renderDetail()}
388
+ />
389
+ );
390
+ }
391
+
392
+ export default McpScreen;
@@ -0,0 +1,292 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useMemo, useEffect } from "react";
3
+ import { useApp } from "../state/AppContext.js";
4
+ import { useDimensions } from "../state/DimensionsContext.js";
5
+ import { ScrollableList } from "../components/ScrollableList.js";
6
+ import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
7
+ import { useKeyboard } from "../hooks/useKeyboard.js";
8
+ const RECENT_MODELS = [
9
+ {
10
+ id: "recent-1",
11
+ type: "model",
12
+ label: "MoonshotAI: Kimi K2 0905 (exacto)",
13
+ provider: "OpenRouter",
14
+ modelId: "moonshot/kimi-k2-0905",
15
+ configured: true,
16
+ },
17
+ ];
18
+ const ANTHROPIC_MODELS = [
19
+ {
20
+ id: "claude-opus-4",
21
+ type: "model",
22
+ label: "Claude Opus 4",
23
+ provider: "Anthropic",
24
+ modelId: "anthropic/claude-opus-4",
25
+ configured: true,
26
+ },
27
+ {
28
+ id: "claude-sonnet-4",
29
+ type: "model",
30
+ label: "Claude Sonnet 4",
31
+ provider: "Anthropic",
32
+ modelId: "anthropic/claude-sonnet-4",
33
+ configured: true,
34
+ },
35
+ {
36
+ id: "claude-opus-4.1",
37
+ type: "model",
38
+ label: "Claude Opus 4.1",
39
+ provider: "Anthropic",
40
+ modelId: "anthropic/claude-opus-4.1",
41
+ configured: true,
42
+ },
43
+ {
44
+ id: "claude-opus-4.5",
45
+ type: "model",
46
+ label: "Claude Opus 4.5",
47
+ provider: "Anthropic",
48
+ modelId: "anthropic/claude-opus-4.5",
49
+ configured: true,
50
+ },
51
+ {
52
+ id: "claude-3.5-haiku",
53
+ type: "model",
54
+ label: "Claude 3.5 Haiku",
55
+ provider: "Anthropic",
56
+ modelId: "anthropic/claude-3.5-haiku",
57
+ configured: true,
58
+ },
59
+ {
60
+ id: "claude-4.5-haiku",
61
+ type: "model",
62
+ label: "Claude 4.5 Haiku",
63
+ provider: "Anthropic",
64
+ modelId: "anthropic/claude-4.5-haiku",
65
+ configured: true,
66
+ },
67
+ {
68
+ id: "claude-3.7-sonnet",
69
+ type: "model",
70
+ label: "Claude 3.7 Sonnet",
71
+ provider: "Anthropic",
72
+ modelId: "anthropic/claude-3.7-sonnet",
73
+ configured: true,
74
+ },
75
+ {
76
+ id: "claude-sonnet-4.5",
77
+ type: "model",
78
+ label: "Claude Sonnet 4.5",
79
+ provider: "Anthropic",
80
+ modelId: "anthropic/claude-sonnet-4.5",
81
+ configured: true,
82
+ },
83
+ {
84
+ id: "claude-3.5-sonnet-new",
85
+ type: "model",
86
+ label: "Claude 3.5 Sonnet (New)",
87
+ provider: "Anthropic",
88
+ modelId: "anthropic/claude-3-5-sonnet-new",
89
+ configured: true,
90
+ },
91
+ {
92
+ id: "claude-3.5-sonnet-old",
93
+ type: "model",
94
+ label: "Claude 3.5 Sonnet (Old)",
95
+ provider: "Anthropic",
96
+ modelId: "anthropic/claude-3-5-sonnet-old",
97
+ configured: true,
98
+ },
99
+ ];
100
+ const OPENAI_MODELS = [
101
+ {
102
+ id: "gpt-5-codex",
103
+ type: "model",
104
+ label: "GPT-5 Codex",
105
+ provider: "OpenAI",
106
+ modelId: "openai/gpt-5-codex",
107
+ configured: true,
108
+ },
109
+ {
110
+ id: "gpt-5.1-codex",
111
+ type: "model",
112
+ label: "GPT-5.1 Codex",
113
+ provider: "OpenAI",
114
+ modelId: "openai/gpt-5.1-codex",
115
+ configured: true,
116
+ },
117
+ {
118
+ id: "gpt-5.1-codex-max",
119
+ type: "model",
120
+ label: "GPT-5.1 Codex Max",
121
+ provider: "OpenAI",
122
+ modelId: "openai/gpt-5.1-codex-max",
123
+ configured: true,
124
+ },
125
+ {
126
+ id: "gpt-5.1-codex-mini",
127
+ type: "model",
128
+ label: "GPT-5.1 Codex Mini",
129
+ provider: "OpenAI",
130
+ modelId: "openai/gpt-5.1-codex-mini",
131
+ configured: true,
132
+ },
133
+ ];
134
+ const XAI_MODELS = [
135
+ {
136
+ id: "grok-code-fast",
137
+ type: "model",
138
+ label: "Grok Code Fast",
139
+ provider: "xAI",
140
+ modelId: "xai/grok-code-fast",
141
+ configured: false,
142
+ }, // Configured check missing in reference image? No, line is very faint. Assuming not configured or just separator missing.
143
+ ];
144
+ export function ModelSelectorScreen() {
145
+ const { state, dispatch } = useApp();
146
+ const { modelSelector } = state;
147
+ const dimensions = useDimensions();
148
+ // Combine items into display list
149
+ const allItems = useMemo(() => {
150
+ return [
151
+ {
152
+ id: "header-recent",
153
+ type: "header",
154
+ label: "Recently used",
155
+ provider: "OpenRouter",
156
+ }, // Provider column used for right text
157
+ ...RECENT_MODELS,
158
+ {
159
+ id: "header-anthropic",
160
+ type: "header",
161
+ label: "Anthropic",
162
+ configured: true,
163
+ },
164
+ ...ANTHROPIC_MODELS,
165
+ {
166
+ id: "header-openai",
167
+ type: "header",
168
+ label: "OpenAI",
169
+ configured: true,
170
+ },
171
+ ...OPENAI_MODELS,
172
+ { id: "header-xai", type: "header", label: "xAI" },
173
+ ...XAI_MODELS,
174
+ ];
175
+ }, []);
176
+ // Filter items
177
+ const filteredItems = useMemo(() => {
178
+ if (!modelSelector.searchQuery)
179
+ return allItems;
180
+ // Filter only models
181
+ const models = allItems.filter((i) => i.type === "model");
182
+ const fuzzyResults = fuzzyFilter(models, modelSelector.searchQuery, (item) => item.label);
183
+ // If searching, just show matched models without headers to keep it clean, or keep hierarchy?
184
+ // "Switch Model" TUI usually flattens on search.
185
+ return fuzzyResults.map((r) => ({ ...r.item, _matches: r.matches }));
186
+ }, [allItems, modelSelector.searchQuery]);
187
+ // Ensure selection is valid (not a header) on mount or search change
188
+ useEffect(() => {
189
+ // If current selection is a header or out of bounds, find first model
190
+ const current = filteredItems[modelSelector.selectedIndex];
191
+ if (!current || current.type === "header") {
192
+ const firstModelIndex = filteredItems.findIndex((i) => i.type === "model");
193
+ if (firstModelIndex !== -1 &&
194
+ firstModelIndex !== modelSelector.selectedIndex) {
195
+ dispatch({ type: "MODEL_SELECTOR_SELECT", index: firstModelIndex });
196
+ }
197
+ }
198
+ }, [filteredItems, modelSelector.selectedIndex, dispatch]);
199
+ // Handle keyboard
200
+ useKeyboard((event) => {
201
+ // Search input handling
202
+ if (event.name === "backspace" || event.name === "delete") {
203
+ dispatch({
204
+ type: "MODEL_SELECTOR_SET_SEARCH",
205
+ query: modelSelector.searchQuery.slice(0, -1),
206
+ });
207
+ return;
208
+ }
209
+ // Toggle Task Size
210
+ if (event.name === "tab") {
211
+ const newSize = modelSelector.taskSize === "large" ? "small" : "large";
212
+ dispatch({ type: "MODEL_SELECTOR_SET_TASK_SIZE", size: newSize });
213
+ return;
214
+ }
215
+ // Navigation
216
+ if (event.name === "up" || (event.ctrl && event.name === "p")) {
217
+ // Standard navigation
218
+ let newIndex = modelSelector.selectedIndex - 1;
219
+ // Skip headers while moving up
220
+ while (newIndex >= 0 && filteredItems[newIndex]?.type === "header") {
221
+ newIndex--;
222
+ }
223
+ if (newIndex >= 0) {
224
+ dispatch({ type: "MODEL_SELECTOR_SELECT", index: newIndex });
225
+ }
226
+ return;
227
+ }
228
+ if (event.name === "down" || (event.ctrl && event.name === "n")) {
229
+ let newIndex = modelSelector.selectedIndex + 1;
230
+ // Skip headers while moving down
231
+ while (newIndex < filteredItems.length &&
232
+ filteredItems[newIndex]?.type === "header") {
233
+ newIndex++;
234
+ }
235
+ if (newIndex < filteredItems.length) {
236
+ dispatch({ type: "MODEL_SELECTOR_SELECT", index: newIndex });
237
+ }
238
+ return;
239
+ }
240
+ if (event.name === "enter" || event.name === "return") {
241
+ // Just log selection for now or exit
242
+ // dispatch({ type: 'NAVIGATE', route: { screen: 'plugins' } });
243
+ return;
244
+ }
245
+ // Regular text input (single character keys)
246
+ if (event.name.length === 1 &&
247
+ !event.ctrl &&
248
+ !event.shift) {
249
+ dispatch({
250
+ type: "MODEL_SELECTOR_SET_SEARCH",
251
+ query: modelSelector.searchQuery + event.name,
252
+ });
253
+ return;
254
+ }
255
+ });
256
+ // Render list item
257
+ const renderItem = (item, _index, isSelected) => {
258
+ if (item.type === "header") {
259
+ // Header style: "Name ----------------------------- Status"
260
+ const status = item.configured ? "✔ Configured" : "";
261
+ return (_jsxs("box", { margin: 0, children: [_jsxs("text", { fg: "gray", children: [item.label, " "] }), _jsx("text", { fg: "#888888", children: "\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF" }), status && _jsxs("text", { fg: "green", children: [" ", status] }), item.provider && _jsxs("text", { fg: "blue", children: [" ", item.provider] })] }));
262
+ }
263
+ // Model item
264
+ // Matches highlighting
265
+ const matches = item._matches;
266
+ const labelSegment = matches ? highlightMatches(item.label, matches) : null;
267
+ const Label = () => (_jsx("text", { fg: isSelected ? "white" : "white", children: labelSegment
268
+ ? labelSegment.map((seg, i) => (_jsx("text", { fg: seg.highlighted
269
+ ? isSelected
270
+ ? "white"
271
+ : "cyan"
272
+ : isSelected
273
+ ? "white"
274
+ : "white", children: seg.highlighted ? _jsx("u", { children: _jsx("strong", { children: seg.text }) }) : seg.text }, i)))
275
+ : item.label }));
276
+ if (isSelected) {
277
+ return (_jsxs("box", { overflow: "hidden", children: [_jsx("text", { bg: "magenta", fg: "white", children: " " }), _jsx("text", { bg: "magenta", fg: "white", children: _jsx(Label, {}) }), _jsx("box", { flexGrow: 1, children: _jsx("text", { bg: "magenta", children: " " }) }), item.provider && (_jsxs("text", { bg: "magenta", fg: "white", children: [item.provider, " "] }))] }));
278
+ }
279
+ else {
280
+ return (_jsxs("box", { overflow: "hidden", children: [_jsx(Label, {}), _jsx("box", { flexGrow: 1 }), item.provider && _jsx("text", { fg: "gray", children: item.provider })] }));
281
+ }
282
+ };
283
+ const footerHints = "↑↓ choose • tab toggle type • enter choose • esc exit";
284
+ // Available height calculation
285
+ // Header: 1 line (title) + 1 line (search) + 1 line (separator) = 3 lines?
286
+ // Title line: Switch Model /// (o) Large
287
+ // Cursor line: > c
288
+ // Separator: handled by list? or explicit?
289
+ const listHeight = Math.max(5, dimensions.contentHeight - 5);
290
+ return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, showScrollIndicators: false }) }), _jsx("box", { height: 1, children: _jsx("text", { fg: "#888888", children: footerHints }) })] }));
291
+ }
292
+ export default ModelSelectorScreen;