claudeup 3.8.0 → 3.10.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.
@@ -0,0 +1,401 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import type {
5
+ SkillSource,
6
+ SkillInfo,
7
+ SkillFrontmatter,
8
+ GitTreeResponse,
9
+ } from "../types/index.js";
10
+ import { RECOMMENDED_SKILLS } from "../data/skill-repos.js";
11
+
12
+ const SKILLS_API_BASE =
13
+ "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
14
+
15
+ // ─── In-process cache ─────────────────────────────────────────────────────────
16
+
17
+ interface TreeCacheEntry {
18
+ etag: string;
19
+ tree: GitTreeResponse;
20
+ fetchedAt: number;
21
+ }
22
+
23
+ const treeCache = new Map<string, TreeCacheEntry>();
24
+
25
+ // ─── Path helpers ──────────────────────────────────────────────────────────────
26
+
27
+ export function getUserSkillsDir(): string {
28
+ return path.join(os.homedir(), ".claude", "skills");
29
+ }
30
+
31
+ export function getProjectSkillsDir(projectPath?: string): string {
32
+ return path.join(projectPath ?? process.cwd(), ".claude", "skills");
33
+ }
34
+
35
+ // ─── GitHub Tree API ──────────────────────────────────────────────────────────
36
+
37
+ async function fetchGitTree(repo: string): Promise<GitTreeResponse> {
38
+ const cached = treeCache.get(repo);
39
+
40
+ const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
41
+ const headers: Record<string, string> = {
42
+ Accept: "application/vnd.github+json",
43
+ "X-GitHub-Api-Version": "2022-11-28",
44
+ };
45
+
46
+ if (cached?.etag) {
47
+ headers["If-None-Match"] = cached.etag;
48
+ }
49
+
50
+ const token =
51
+ process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
52
+ if (token) {
53
+ headers.Authorization = `Bearer ${token}`;
54
+ }
55
+
56
+ const response = await fetch(url, {
57
+ headers,
58
+ signal: AbortSignal.timeout(10000),
59
+ });
60
+
61
+ if (response.status === 304 && cached) {
62
+ return cached.tree;
63
+ }
64
+
65
+ if (response.status === 403 || response.status === 429) {
66
+ const resetHeader = response.headers.get("X-RateLimit-Reset");
67
+ const resetTime = resetHeader
68
+ ? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
69
+ : "unknown";
70
+ throw new Error(
71
+ `GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`,
72
+ );
73
+ }
74
+
75
+ if (!response.ok) {
76
+ throw new Error(
77
+ `GitHub API error: ${response.status} ${response.statusText}`,
78
+ );
79
+ }
80
+
81
+ const etag = response.headers.get("ETag") || "";
82
+ const tree = (await response.json()) as GitTreeResponse;
83
+
84
+ treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
85
+ return tree;
86
+ }
87
+
88
+ // ─── Frontmatter parser ───────────────────────────────────────────────────────
89
+
90
+ function parseYamlFrontmatter(content: string): Partial<SkillFrontmatter> {
91
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
92
+ if (!frontmatterMatch) return {};
93
+
94
+ const yaml = frontmatterMatch[1];
95
+ const result: Record<string, unknown> = {};
96
+
97
+ for (const line of yaml.split("\n")) {
98
+ const colonIdx = line.indexOf(":");
99
+ if (colonIdx === -1) continue;
100
+
101
+ const key = line.slice(0, colonIdx).trim();
102
+ const rawValue = line.slice(colonIdx + 1).trim();
103
+
104
+ if (!key) continue;
105
+
106
+ // Handle arrays (simple inline: [a, b, c])
107
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
108
+ const items = rawValue
109
+ .slice(1, -1)
110
+ .split(",")
111
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
112
+ .filter(Boolean);
113
+ result[key] = items;
114
+ } else {
115
+ // Strip quotes
116
+ result[key] = rawValue.replace(/^["']|["']$/g, "");
117
+ }
118
+ }
119
+
120
+ return result as Partial<SkillFrontmatter>;
121
+ }
122
+
123
+ export async function fetchSkillFrontmatter(
124
+ skill: SkillInfo,
125
+ ): Promise<SkillFrontmatter> {
126
+ const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
127
+
128
+ const response = await fetch(url, {
129
+ signal: AbortSignal.timeout(10000),
130
+ });
131
+
132
+ if (!response.ok) {
133
+ return {
134
+ name: skill.name,
135
+ description: "(no description)",
136
+ category: "general",
137
+ };
138
+ }
139
+
140
+ const content = await response.text();
141
+ const parsed = parseYamlFrontmatter(content);
142
+
143
+ return {
144
+ name: parsed.name || skill.name,
145
+ description: parsed.description || "(no description)",
146
+ category: parsed.category,
147
+ author: parsed.author,
148
+ version: parsed.version,
149
+ tags: parsed.tags,
150
+ };
151
+ }
152
+
153
+ // ─── Check installation ───────────────────────────────────────────────────────
154
+
155
+ export async function getInstalledSkillNames(
156
+ scope: "user" | "project",
157
+ projectPath?: string,
158
+ ): Promise<Set<string>> {
159
+ const dir =
160
+ scope === "user"
161
+ ? getUserSkillsDir()
162
+ : getProjectSkillsDir(projectPath);
163
+
164
+ const installed = new Set<string>();
165
+
166
+ try {
167
+ if (!(await fs.pathExists(dir))) return installed;
168
+ const entries = await fs.readdir(dir);
169
+ for (const entry of entries) {
170
+ const skillMd = path.join(dir, entry, "SKILL.md");
171
+ if (await fs.pathExists(skillMd)) {
172
+ installed.add(entry);
173
+ }
174
+ }
175
+ } catch {
176
+ // ignore
177
+ }
178
+
179
+ return installed;
180
+ }
181
+
182
+ // ─── Firebase Skills API ──────────────────────────────────────────────────────
183
+
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ function mapApiSkillToSkillInfo(raw: any): SkillInfo {
186
+ const repo = (raw.repo as string) || "unknown";
187
+ const skillPath = (raw.skillPath as string) || (raw.name as string) || "";
188
+ const source: SkillSource = {
189
+ label: repo,
190
+ repo,
191
+ skillsPath: "",
192
+ };
193
+ return {
194
+ id: `${repo}/${skillPath}`,
195
+ name: (raw.name as string) || skillPath,
196
+ description: (raw.description as string) || "",
197
+ source,
198
+ repoPath: skillPath ? `${skillPath}/SKILL.md` : "SKILL.md",
199
+ gitBlobSha: "",
200
+ frontmatter: null,
201
+ installed: false,
202
+ installedScope: null,
203
+ hasUpdate: false,
204
+ stars: typeof raw.stars === "number" ? raw.stars : undefined,
205
+ };
206
+ }
207
+
208
+ export async function fetchPopularSkills(limit = 30): Promise<SkillInfo[]> {
209
+ try {
210
+ const res = await fetch(
211
+ `${SKILLS_API_BASE}/search?q=development&limit=${limit}&sortBy=stars`,
212
+ { signal: AbortSignal.timeout(10000) },
213
+ );
214
+ if (!res.ok) return [];
215
+ const data = (await res.json()) as { skills?: unknown[] };
216
+ return (data.skills || []).map(mapApiSkillToSkillInfo);
217
+ } catch {
218
+ return [];
219
+ }
220
+ }
221
+
222
+ // ─── Fetch available skills ───────────────────────────────────────────────────
223
+
224
+ export async function fetchAvailableSkills(
225
+ _repos: SkillSource[],
226
+ projectPath?: string,
227
+ ): Promise<SkillInfo[]> {
228
+ const userInstalled = await getInstalledSkillNames("user");
229
+ const projectInstalled = await getInstalledSkillNames("project", projectPath);
230
+
231
+ const markInstalled = (skill: SkillInfo): SkillInfo => {
232
+ const isUserInstalled = userInstalled.has(skill.name);
233
+ const isProjInstalled = projectInstalled.has(skill.name);
234
+ const installed = isUserInstalled || isProjInstalled;
235
+ const installedScope: "user" | "project" | null = isProjInstalled
236
+ ? "project"
237
+ : isUserInstalled
238
+ ? "user"
239
+ : null;
240
+ return { ...skill, installed, installedScope };
241
+ };
242
+
243
+ // 1. Recommended skills from RECOMMENDED_SKILLS constant (no API call)
244
+ const recommendedSkills: SkillInfo[] = RECOMMENDED_SKILLS.map((rec) => {
245
+ const source: SkillSource = {
246
+ label: rec.repo,
247
+ repo: rec.repo,
248
+ skillsPath: "",
249
+ };
250
+ const skill: SkillInfo = {
251
+ id: `${rec.repo}/${rec.skillPath}`,
252
+ name: rec.name,
253
+ description: rec.description,
254
+ source,
255
+ repoPath: `${rec.skillPath}/SKILL.md`,
256
+ gitBlobSha: "",
257
+ frontmatter: null,
258
+ installed: false,
259
+ installedScope: null,
260
+ hasUpdate: false,
261
+ isRecommended: true,
262
+ };
263
+ return markInstalled(skill);
264
+ });
265
+
266
+ // 2. Fetch popular skills from Firebase API
267
+ const popular = await fetchPopularSkills(30);
268
+ const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
269
+
270
+ // 3. Enrich recommended skills with GitHub repo stars
271
+ // Fetch stars for each unique repo (typically ~7 repos, parallel)
272
+ const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
273
+ const repoStars = new Map<string, number>();
274
+ try {
275
+ const starResults = await Promise.allSettled(
276
+ uniqueRepos.map(async (repo) => {
277
+ const res = await fetch(`https://api.github.com/repos/${repo}`, {
278
+ headers: { Accept: "application/vnd.github+json" },
279
+ signal: AbortSignal.timeout(5000),
280
+ });
281
+ if (!res.ok) return;
282
+ const data = (await res.json()) as { stargazers_count?: number };
283
+ if (data.stargazers_count) repoStars.set(repo, data.stargazers_count);
284
+ }),
285
+ );
286
+ } catch {
287
+ // Non-fatal — stars are cosmetic
288
+ }
289
+ for (const rec of recommendedSkills) {
290
+ rec.stars = repoStars.get(rec.source.repo) || undefined;
291
+ }
292
+
293
+ // 4. Combine: recommended first, then popular (dedup by name)
294
+ const seen = new Set<string>(recommendedSkills.map((s) => s.name));
295
+ const deduped = popularSkills.filter((s) => !seen.has(s.name));
296
+
297
+ return [...recommendedSkills, ...deduped];
298
+ }
299
+
300
+ // ─── Install / Uninstall ──────────────────────────────────────────────────────
301
+
302
+ export async function installSkill(
303
+ skill: SkillInfo,
304
+ scope: "user" | "project",
305
+ projectPath?: string,
306
+ ): Promise<void> {
307
+ // Try multiple URL patterns — repos structure SKILL.md differently
308
+ const repo = skill.source.repo;
309
+ const repoPath = skill.repoPath.replace(/\/SKILL\.md$/, "");
310
+ const candidates = [
311
+ `https://raw.githubusercontent.com/${repo}/HEAD/${repoPath}/SKILL.md`,
312
+ `https://raw.githubusercontent.com/${repo}/main/${repoPath}/SKILL.md`,
313
+ `https://raw.githubusercontent.com/${repo}/master/${repoPath}/SKILL.md`,
314
+ `https://raw.githubusercontent.com/${repo}/HEAD/SKILL.md`,
315
+ `https://raw.githubusercontent.com/${repo}/main/SKILL.md`,
316
+ ];
317
+
318
+ let content: string | null = null;
319
+ for (const url of candidates) {
320
+ try {
321
+ const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
322
+ if (response.ok) {
323
+ content = await response.text();
324
+ break;
325
+ }
326
+ } catch {
327
+ continue;
328
+ }
329
+ }
330
+
331
+ if (!content) {
332
+ throw new Error(
333
+ `Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`,
334
+ );
335
+ }
336
+
337
+ const installDir =
338
+ scope === "user"
339
+ ? path.join(getUserSkillsDir(), skill.name)
340
+ : path.join(getProjectSkillsDir(projectPath), skill.name);
341
+
342
+ await fs.ensureDir(installDir);
343
+ await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
344
+ }
345
+
346
+ export async function uninstallSkill(
347
+ skillName: string,
348
+ scope: "user" | "project",
349
+ projectPath?: string,
350
+ ): Promise<void> {
351
+ const installDir =
352
+ scope === "user"
353
+ ? path.join(getUserSkillsDir(), skillName)
354
+ : path.join(getProjectSkillsDir(projectPath), skillName);
355
+
356
+ const skillMdPath = path.join(installDir, "SKILL.md");
357
+
358
+ if (await fs.pathExists(skillMdPath)) {
359
+ await fs.remove(skillMdPath);
360
+ }
361
+
362
+ // Try to remove directory if empty
363
+ try {
364
+ await fs.rmdir(installDir);
365
+ } catch {
366
+ // Ignore if not empty or doesn't exist
367
+ }
368
+ }
369
+
370
+ // ─── Check installed skills from file system ──────────────────────────────────
371
+
372
+ export async function getInstalledSkillsFromFs(
373
+ projectPath?: string,
374
+ ): Promise<{ name: string; scope: "user" | "project" }[]> {
375
+ const result: { name: string; scope: "user" | "project" }[] = [];
376
+
377
+ const userDir = getUserSkillsDir();
378
+ const projDir = getProjectSkillsDir(projectPath);
379
+
380
+ const scopedDirs: Array<[string, "user" | "project"]> = [
381
+ [userDir, "user"],
382
+ [projDir, "project"],
383
+ ];
384
+
385
+ for (const [dir, scope] of scopedDirs) {
386
+ try {
387
+ if (!(await fs.pathExists(dir))) continue;
388
+ const entries = await fs.readdir(dir);
389
+ for (const entry of entries) {
390
+ const skillMd = path.join(dir, entry, "SKILL.md");
391
+ if (await fs.pathExists(skillMd)) {
392
+ result.push({ name: entry, scope });
393
+ }
394
+ }
395
+ } catch {
396
+ // ignore
397
+ }
398
+ }
399
+
400
+ return result;
401
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Skills API client — calls our Firebase aggregator function
3
+ * instead of hitting SkillsMP/GitHub APIs directly.
4
+ *
5
+ * The Firebase function handles:
6
+ * - SkillsMP keyword + AI search (with server-side API key)
7
+ * - GitHub Tree API with ETag caching
8
+ * - Recommended skills list
9
+ */
10
+ // TODO: Update to production URL after Firebase deploy
11
+ const SKILLS_API_BASE = process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
12
+ /**
13
+ * Search skills across all sources (SkillsMP keyword + AI search)
14
+ */
15
+ export async function searchSkills(query, options) {
16
+ const params = new URLSearchParams({ q: query });
17
+ if (options?.page)
18
+ params.set("page", String(options.page));
19
+ if (options?.limit)
20
+ params.set("limit", String(options.limit));
21
+ try {
22
+ const res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
23
+ signal: AbortSignal.timeout(12000),
24
+ });
25
+ if (!res.ok)
26
+ return [];
27
+ const data = (await res.json());
28
+ return data.skills || [];
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ /**
35
+ * Fetch skills from a specific GitHub repo (via our proxy)
36
+ */
37
+ export async function fetchRepoSkills(owner, repo) {
38
+ try {
39
+ const res = await fetch(`${SKILLS_API_BASE}/repo/${owner}/${repo}`, {
40
+ signal: AbortSignal.timeout(10000),
41
+ });
42
+ if (!res.ok)
43
+ return [];
44
+ const data = (await res.json());
45
+ return data.skills || [];
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ /**
52
+ * Get recommended skills list (curated, server-side)
53
+ */
54
+ export async function fetchRecommendedSkills() {
55
+ try {
56
+ const res = await fetch(`${SKILLS_API_BASE}/recommended`, {
57
+ signal: AbortSignal.timeout(5000),
58
+ });
59
+ if (!res.ok)
60
+ return [];
61
+ const data = (await res.json());
62
+ return data.skills || [];
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Skills API client — calls our Firebase aggregator function
3
+ * instead of hitting SkillsMP/GitHub APIs directly.
4
+ *
5
+ * The Firebase function handles:
6
+ * - SkillsMP keyword + AI search (with server-side API key)
7
+ * - GitHub Tree API with ETag caching
8
+ * - Recommended skills list
9
+ */
10
+
11
+ // TODO: Update to production URL after Firebase deploy
12
+ const SKILLS_API_BASE =
13
+ process.env.SKILLS_API_URL || "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
14
+
15
+ export interface SkillSearchResult {
16
+ name: string;
17
+ displayName?: string;
18
+ repo: string;
19
+ skillPath: string;
20
+ description?: string;
21
+ source?: string;
22
+ stars?: number;
23
+ installCommand?: string;
24
+ }
25
+
26
+ export interface RepoSkillResult {
27
+ name: string;
28
+ skillPath: string;
29
+ treeSha?: string;
30
+ description?: string;
31
+ }
32
+
33
+ /**
34
+ * Search skills across all sources (SkillsMP keyword + AI search)
35
+ */
36
+ export async function searchSkills(
37
+ query: string,
38
+ options?: { page?: number; limit?: number },
39
+ ): Promise<SkillSearchResult[]> {
40
+ const params = new URLSearchParams({ q: query });
41
+ if (options?.page) params.set("page", String(options.page));
42
+ if (options?.limit) params.set("limit", String(options.limit));
43
+
44
+ try {
45
+ const res = await fetch(`${SKILLS_API_BASE}/search?${params}`, {
46
+ signal: AbortSignal.timeout(12000),
47
+ });
48
+ if (!res.ok) return [];
49
+ const data = (await res.json()) as { skills: SkillSearchResult[] };
50
+ return data.skills || [];
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Fetch skills from a specific GitHub repo (via our proxy)
58
+ */
59
+ export async function fetchRepoSkills(
60
+ owner: string,
61
+ repo: string,
62
+ ): Promise<RepoSkillResult[]> {
63
+ try {
64
+ const res = await fetch(`${SKILLS_API_BASE}/repo/${owner}/${repo}`, {
65
+ signal: AbortSignal.timeout(10000),
66
+ });
67
+ if (!res.ok) return [];
68
+ const data = (await res.json()) as { skills: RepoSkillResult[] };
69
+ return data.skills || [];
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Get recommended skills list (curated, server-side)
77
+ */
78
+ export async function fetchRecommendedSkills(): Promise<SkillSearchResult[]> {
79
+ try {
80
+ const res = await fetch(`${SKILLS_API_BASE}/recommended`, {
81
+ signal: AbortSignal.timeout(5000),
82
+ });
83
+ if (!res.ok) return [];
84
+ const data = (await res.json()) as { skills: SkillSearchResult[] };
85
+ return data.skills || [];
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
@@ -116,7 +116,77 @@ export type Screen =
116
116
  | "plugins"
117
117
  | "statusline"
118
118
  | "cli-tools"
119
- | "env-vars";
119
+ | "env-vars"
120
+ | "skills";
121
+
122
+ // ─── Skill Types ──────────────────────────────────────────────────────────────
123
+
124
+ /** A configured skill repository (entry in skill-repos.ts) */
125
+ export interface SkillSource {
126
+ /** Human-readable label, e.g. "vercel-labs/agent-skills" */
127
+ label: string;
128
+ /** GitHub owner/repo, e.g. "vercel-labs/agent-skills" */
129
+ repo: string;
130
+ /** Sub-path within the repo where skills live, e.g. "skills" */
131
+ skillsPath: string;
132
+ }
133
+
134
+ /** YAML frontmatter extracted from a SKILL.md file */
135
+ export interface SkillFrontmatter {
136
+ name: string;
137
+ description: string;
138
+ category?: string;
139
+ author?: string;
140
+ version?: string;
141
+ tags?: string[];
142
+ [key: string]: unknown;
143
+ }
144
+
145
+ /** A skill entry from the available-skills list (may or may not be installed) */
146
+ export interface SkillInfo {
147
+ /** Unique key: "{owner}/{repo}/{path}" */
148
+ id: string;
149
+ /** Slug used as directory name: last segment of path */
150
+ name: string;
151
+ /** Source repo this skill comes from */
152
+ source: SkillSource;
153
+ /** Relative path within repo, e.g. "skills/researcher/SKILL.md" */
154
+ repoPath: string;
155
+ /** Git blob SHA from Tree API response */
156
+ gitBlobSha: string;
157
+ /** Parsed frontmatter (null if not yet fetched) */
158
+ frontmatter: SkillFrontmatter | null;
159
+ /** Whether the skill is installed (either scope) */
160
+ installed: boolean;
161
+ /** Scope of the installed copy */
162
+ installedScope: "user" | "project" | null;
163
+ /** True when lock file SHA differs from Tree API SHA */
164
+ hasUpdate: boolean;
165
+ /** Whether this is a recommended skill */
166
+ isRecommended?: boolean;
167
+ /** GitHub star count from the skills API */
168
+ stars?: number;
169
+ /** Short description (from API, before frontmatter is loaded) */
170
+ description?: string;
171
+ }
172
+
173
+ // ─── GitHub Tree API types ────────────────────────────────────────────────────
174
+
175
+ export interface GitTreeItem {
176
+ path: string;
177
+ mode: string;
178
+ type: "blob" | "tree";
179
+ sha: string;
180
+ size?: number;
181
+ url: string;
182
+ }
183
+
184
+ export interface GitTreeResponse {
185
+ sha: string;
186
+ url: string;
187
+ tree: GitTreeItem[];
188
+ truncated: boolean;
189
+ }
120
190
 
121
191
  // MCP Registry Types (registry.modelcontextprotocol.io)
122
192
  export interface McpRegistryServer {
package/src/ui/App.js CHANGED
@@ -5,7 +5,7 @@ import fs from "node:fs";
5
5
  import { AppProvider, useApp, useNavigation, useModal, } from "./state/AppContext.js";
6
6
  import { DimensionsProvider, useDimensions, } from "./state/DimensionsContext.js";
7
7
  import { ModalContainer } from "./components/modals/index.js";
8
- import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, } from "./screens/index.js";
8
+ import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
9
9
  import { repairAllMarketplaces } from "../services/local-marketplace.js";
10
10
  import { migrateMarketplaceRename } from "../services/claude-settings.js";
11
11
  import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
@@ -34,6 +34,8 @@ function Router() {
34
34
  return _jsx(ModelSelectorScreen, {});
35
35
  case "profiles":
36
36
  return _jsx(ProfilesScreen, {});
37
+ case "skills":
38
+ return _jsx(SkillsScreen, {});
37
39
  default:
38
40
  return _jsx(PluginsScreen, {});
39
41
  }
@@ -84,26 +86,30 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
84
86
  "cli-tools",
85
87
  "model-selector",
86
88
  "profiles",
89
+ "skills",
87
90
  ].includes(state.currentRoute.screen);
88
91
  if (isTopLevel) {
89
92
  if (input === "1")
90
93
  navigateToScreen("plugins");
91
94
  else if (input === "2")
92
- navigateToScreen("mcp");
95
+ navigateToScreen("skills");
93
96
  else if (input === "3")
94
- navigateToScreen("settings");
97
+ navigateToScreen("mcp");
95
98
  else if (input === "4")
96
- navigateToScreen("cli-tools");
99
+ navigateToScreen("settings");
97
100
  else if (input === "5")
98
101
  navigateToScreen("profiles");
102
+ else if (input === "6")
103
+ navigateToScreen("cli-tools");
99
104
  // Tab navigation cycling
100
105
  if (key.tab) {
101
106
  const screens = [
102
107
  "plugins",
108
+ "skills",
103
109
  "mcp",
104
110
  "settings",
105
- "cli-tools",
106
111
  "profiles",
112
+ "cli-tools",
107
113
  ];
108
114
  const currentIndex = screens.indexOf(state.currentRoute.screen);
109
115
  if (currentIndex !== -1) {
@@ -138,9 +144,9 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
138
144
  ? This help
139
145
 
140
146
  Quick Navigation
141
- 1 Plugins 4 CLI Tools
142
- 2 MCP Servers 5 Profiles
143
- 3 Settings
147
+ 1 Plugins 4 Settings
148
+ 2 Skills 5 Profiles
149
+ 3 MCP Servers 6 CLI Tools
144
150
 
145
151
  Plugin Actions
146
152
  u Update d Uninstall