claudeup 4.32.0 → 4.33.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.
@@ -9,6 +9,7 @@ import {
9
9
  getCategoryDisplayName,
10
10
  categoryOrder,
11
11
  } from "../../data/mcp-servers.js";
12
+ import { fetchCuratedMcpServers } from "../../services/mcp-registry.js";
12
13
  import {
13
14
  addMcpServer,
14
15
  removeMcpServer,
@@ -33,7 +34,10 @@ export function McpScreen() {
33
34
  const fetchData = useCallback(async () => {
34
35
  dispatch({ type: "MCP_DATA_LOADING" });
35
36
  try {
36
- const serversByCategory = getMcpServersByCategory();
37
+ const curatedServers = await fetchCuratedMcpServers(
38
+ AbortSignal.timeout(10_000),
39
+ );
40
+ const serversByCategory = getMcpServersByCategory(curatedServers);
37
41
  const installedServers = await getInstalledMcpServers(state.projectPath);
38
42
  const enabledServers = await getEnabledMcpServers(state.projectPath);
39
43
 
@@ -70,7 +74,7 @@ export function McpScreen() {
70
74
  const allListItems = useMemo((): McpListItem[] => {
71
75
  if (mcp.servers.status !== "success") return [];
72
76
 
73
- const serversByCategory = getMcpServersByCategory();
77
+ const serversByCategory = getMcpServersByCategory(mcp.servers.data);
74
78
  const items: McpListItem[] = [];
75
79
 
76
80
  for (const category of categoryOrder) {
@@ -86,7 +90,7 @@ export function McpScreen() {
86
90
  }
87
91
 
88
92
  return items;
89
- }, [mcp.servers.status, mcp.installedServers]);
93
+ }, [mcp.servers, mcp.installedServers]);
90
94
 
91
95
  useKeyboard((event) => {
92
96
  if (state.isSearching || state.modal) return;
@@ -142,10 +146,14 @@ export function McpScreen() {
142
146
  let config: McpServerConfig;
143
147
 
144
148
  if (server.type === "http") {
145
- config = { type: "http", url: server.url! };
149
+ if (!server.url) throw new Error(`${server.name} is missing its HTTP URL`);
150
+ config = { type: "http", url: server.url };
146
151
  } else {
152
+ if (!server.command) {
153
+ throw new Error(`${server.name} is missing its launch command`);
154
+ }
147
155
  config = {
148
- command: server.command!,
156
+ command: server.command,
149
157
  args: server.args ? [...server.args] : undefined,
150
158
  env: server.env ? { ...server.env } : undefined,
151
159
  };
@@ -18,8 +18,11 @@ import {
18
18
  refreshAllMarketplaces,
19
19
  clearMarketplaceCache,
20
20
  getLocalMarketplacesInfo,
21
+ isInstalledInScope,
22
+ resolveScopeAction,
21
23
  saveInstalledPluginVersion,
22
24
  type PluginInfo,
25
+ type ScopeStatus,
23
26
  } from "../../services/plugin-manager.js";
24
27
  import {
25
28
  setMcpEnvVar,
@@ -605,10 +608,10 @@ export function PluginsScreen() {
605
608
 
606
609
  const buildScopeLabel = (
607
610
  name: string,
608
- scope: { enabled?: boolean; version?: string } | undefined,
611
+ scope: ScopeStatus | undefined,
609
612
  desc: string,
610
613
  ) => {
611
- const installed = scope?.enabled;
614
+ const installed = isInstalledInScope(scope);
612
615
  const ver = scope?.version;
613
616
  const hasUpdate =
614
617
  ver &&
@@ -619,6 +622,10 @@ export function PluginsScreen() {
619
622
  label += ` (${desc})`;
620
623
  if (ver) label += ` v${ver}`;
621
624
  if (hasUpdate) label += ` → v${latestVersion}`;
625
+ // Enabled in settings with nothing installed. Without saying so the
626
+ // row reads exactly like a scope that was never enabled, and the
627
+ // user cannot tell that picking it repairs rather than adds.
628
+ if (!installed && scope?.enabled) label += " — not installed, repairs";
622
629
  return label;
623
630
  };
624
631
 
@@ -650,8 +657,6 @@ export function PluginsScreen() {
650
657
  : scopeValue === "project"
651
658
  ? plugin.projectScope
652
659
  : plugin.localScope;
653
- const isInstalledInScope = selectedScope?.enabled;
654
- const installedVersion = selectedScope?.version;
655
660
  const scopeLabel =
656
661
  scopeValue === "user"
657
662
  ? "User"
@@ -659,20 +664,9 @@ export function PluginsScreen() {
659
664
  ? "Project"
660
665
  : "Local";
661
666
 
662
- const hasUpdateInScope =
663
- isInstalledInScope &&
664
- installedVersion &&
665
- latestVersion !== "0.0.0" &&
666
- installedVersion !== latestVersion;
667
-
668
- let action: "update" | "install" | "uninstall";
669
- if (isInstalledInScope && hasUpdateInScope) {
670
- action = "update";
671
- } else if (isInstalledInScope) {
672
- action = "uninstall";
673
- } else {
674
- action = "install";
675
- }
667
+ // Decided from the registry-backed version, not the `enabledPlugins`
668
+ // flag alone — see resolveScopeAction.
669
+ const action = resolveScopeAction(selectedScope, latestVersion);
676
670
 
677
671
  try {
678
672
  const scope = scopeValue as PluginScope;
@@ -843,23 +837,12 @@ export function PluginsScreen() {
843
837
  : scope === "project"
844
838
  ? plugin.projectScope
845
839
  : plugin.localScope;
846
- const isInstalledInScope = scopeData?.enabled;
847
- const installedVersion = scopeData?.version;
848
-
849
- const hasUpdateInScope =
850
- isInstalledInScope &&
851
- installedVersion &&
852
- latestVersion !== "0.0.0" &&
853
- installedVersion !== latestVersion;
854
-
855
- let action: "update" | "install" | "uninstall";
856
- if (isInstalledInScope && hasUpdateInScope) {
857
- action = "update";
858
- } else if (isInstalledInScope) {
859
- action = "uninstall";
860
- } else {
861
- action = "install";
862
- }
840
+
841
+ // This used to key off `scopeData.enabled` alone, so a plugin the list
842
+ // had just labelled "not installed" answered its own scope key by
843
+ // uninstalling itself — the `enabledPlugins` flag was set, the registry
844
+ // entry was missing, and only the flag was consulted.
845
+ const action = resolveScopeAction(scopeData, latestVersion);
863
846
 
864
847
  try {
865
848
  if (action === "uninstall") {
@@ -15,8 +15,8 @@ import {
15
15
  installSkill,
16
16
  uninstallSkill,
17
17
  } from "../../services/skills-manager.js";
18
- import { searchSkills } from "../../services/skillsmp-client.js";
19
- import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS, RECOMMENDED_SKILL_SETS, classifyStarReliability } from "../../data/skill-repos.js";
18
+ import { fetchCuratedSkillsCatalog, searchSkills } from "../../services/skillsmp-client.js";
19
+ import { DEFAULT_SKILL_REPOS, classifyStarReliability, type RecommendedSkill } from "../../data/skill-repos.js";
20
20
  import type { SkillInfo, SkillSetInfo, SkillSource } from "../../types/index.js";
21
21
  import { buildSkillBrowserItems } from "../adapters/skillsAdapter.js";
22
22
  import type { SkillBrowserItem } from "../adapters/skillsAdapter.js";
@@ -28,6 +28,9 @@ export function SkillsScreen() {
28
28
  const { skills: skillsState } = state;
29
29
  const modal = useModal();
30
30
  const dimensions = useDimensions();
31
+ const [curatedSkills, setCuratedSkills] = useState<RecommendedSkill[]>([]);
32
+ const [skillSets, setSkillSets] = useState<SkillSetInfo[]>([]);
33
+ const [expandedSets, setExpandedSets] = useState<Set<string>>(new Set());
31
34
 
32
35
  const isSearchActive =
33
36
  state.isSearching &&
@@ -39,9 +42,30 @@ export function SkillsScreen() {
39
42
  const fetchData = useCallback(async () => {
40
43
  dispatch({ type: "SKILLS_DATA_LOADING" });
41
44
  try {
45
+ const catalog = await fetchCuratedSkillsCatalog();
46
+ setCuratedSkills(catalog.skills);
47
+ setSkillSets((previous) =>
48
+ catalog.skillSets.map((skillSet) => {
49
+ const existing = previous.find((item) => item.repo === skillSet.repo);
50
+ return existing
51
+ ? { ...existing, ...skillSet, id: skillSet.repo }
52
+ : {
53
+ id: skillSet.repo,
54
+ name: skillSet.name,
55
+ description: skillSet.description,
56
+ repo: skillSet.repo,
57
+ icon: skillSet.icon,
58
+ stars: skillSet.stars,
59
+ skills: [],
60
+ loaded: false,
61
+ loading: false,
62
+ };
63
+ }),
64
+ );
42
65
  const skills = await fetchAvailableSkills(
43
66
  DEFAULT_SKILL_REPOS,
44
67
  state.projectPath,
68
+ catalog.skills,
45
69
  );
46
70
  dispatch({ type: "SKILLS_DATA_SUCCESS", skills });
47
71
  } catch (error) {
@@ -159,20 +183,6 @@ export function SkillsScreen() {
159
183
 
160
184
  // ── Skill Sets state ──────────────────────────────────────────────────────
161
185
 
162
- const [skillSets, setSkillSets] = useState<SkillSetInfo[]>(() =>
163
- RECOMMENDED_SKILL_SETS.map((rs) => ({
164
- id: rs.repo,
165
- name: rs.name,
166
- description: rs.description,
167
- repo: rs.repo,
168
- icon: rs.icon,
169
- stars: rs.stars,
170
- skills: [],
171
- loaded: false,
172
- loading: false,
173
- })),
174
- );
175
- const [expandedSets, setExpandedSets] = useState<Set<string>>(new Set());
176
186
 
177
187
  // Re-mark installed status on child skills when disk state changes
178
188
  useEffect(() => {
@@ -282,7 +292,7 @@ export function SkillsScreen() {
282
292
  // ── Derived data ──────────────────────────────────────────────────────────
283
293
 
284
294
  const staticRecommended = useMemo((): SkillInfo[] => {
285
- return RECOMMENDED_SKILLS.map((r) => {
295
+ return curatedSkills.map((r) => {
286
296
  const slug = r.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
287
297
  const isUser = installedFromDisk.user.has(slug) || installedFromDisk.user.has(r.name);
288
298
  const isProj = installedFromDisk.project.has(slug) || installedFromDisk.project.has(r.name);
@@ -302,7 +312,7 @@ export function SkillsScreen() {
302
312
  starReliability: classifyStarReliability(r.repo, r.stars),
303
313
  };
304
314
  });
305
- }, [installedFromDisk]);
315
+ }, [curatedSkills, installedFromDisk]);
306
316
 
307
317
  const mergedRecommended = useMemo((): SkillInfo[] => {
308
318
  if (skillsState.skills.status !== "success") return staticRecommended;