claudeup 3.8.0 → 3.9.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,328 @@
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
+
11
+ // ─── In-process cache ─────────────────────────────────────────────────────────
12
+
13
+ interface TreeCacheEntry {
14
+ etag: string;
15
+ tree: GitTreeResponse;
16
+ fetchedAt: number;
17
+ }
18
+
19
+ const treeCache = new Map<string, TreeCacheEntry>();
20
+
21
+ // ─── Path helpers ──────────────────────────────────────────────────────────────
22
+
23
+ export function getUserSkillsDir(): string {
24
+ return path.join(os.homedir(), ".claude", "skills");
25
+ }
26
+
27
+ export function getProjectSkillsDir(projectPath?: string): string {
28
+ return path.join(projectPath ?? process.cwd(), ".claude", "skills");
29
+ }
30
+
31
+ // ─── GitHub Tree API ──────────────────────────────────────────────────────────
32
+
33
+ async function fetchGitTree(repo: string): Promise<GitTreeResponse> {
34
+ const cached = treeCache.get(repo);
35
+
36
+ const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
37
+ const headers: Record<string, string> = {
38
+ Accept: "application/vnd.github+json",
39
+ "X-GitHub-Api-Version": "2022-11-28",
40
+ };
41
+
42
+ if (cached?.etag) {
43
+ headers["If-None-Match"] = cached.etag;
44
+ }
45
+
46
+ const token =
47
+ process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
48
+ if (token) {
49
+ headers.Authorization = `Bearer ${token}`;
50
+ }
51
+
52
+ const response = await fetch(url, {
53
+ headers,
54
+ signal: AbortSignal.timeout(10000),
55
+ });
56
+
57
+ if (response.status === 304 && cached) {
58
+ return cached.tree;
59
+ }
60
+
61
+ if (response.status === 403 || response.status === 429) {
62
+ const resetHeader = response.headers.get("X-RateLimit-Reset");
63
+ const resetTime = resetHeader
64
+ ? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
65
+ : "unknown";
66
+ throw new Error(
67
+ `GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`,
68
+ );
69
+ }
70
+
71
+ if (!response.ok) {
72
+ throw new Error(
73
+ `GitHub API error: ${response.status} ${response.statusText}`,
74
+ );
75
+ }
76
+
77
+ const etag = response.headers.get("ETag") || "";
78
+ const tree = (await response.json()) as GitTreeResponse;
79
+
80
+ treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
81
+ return tree;
82
+ }
83
+
84
+ // ─── Frontmatter parser ───────────────────────────────────────────────────────
85
+
86
+ function parseYamlFrontmatter(content: string): Partial<SkillFrontmatter> {
87
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
88
+ if (!frontmatterMatch) return {};
89
+
90
+ const yaml = frontmatterMatch[1];
91
+ const result: Record<string, unknown> = {};
92
+
93
+ for (const line of yaml.split("\n")) {
94
+ const colonIdx = line.indexOf(":");
95
+ if (colonIdx === -1) continue;
96
+
97
+ const key = line.slice(0, colonIdx).trim();
98
+ const rawValue = line.slice(colonIdx + 1).trim();
99
+
100
+ if (!key) continue;
101
+
102
+ // Handle arrays (simple inline: [a, b, c])
103
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
104
+ const items = rawValue
105
+ .slice(1, -1)
106
+ .split(",")
107
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
108
+ .filter(Boolean);
109
+ result[key] = items;
110
+ } else {
111
+ // Strip quotes
112
+ result[key] = rawValue.replace(/^["']|["']$/g, "");
113
+ }
114
+ }
115
+
116
+ return result as Partial<SkillFrontmatter>;
117
+ }
118
+
119
+ export async function fetchSkillFrontmatter(
120
+ skill: SkillInfo,
121
+ ): Promise<SkillFrontmatter> {
122
+ const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
123
+
124
+ const response = await fetch(url, {
125
+ signal: AbortSignal.timeout(10000),
126
+ });
127
+
128
+ if (!response.ok) {
129
+ return {
130
+ name: skill.name,
131
+ description: "(no description)",
132
+ category: "general",
133
+ };
134
+ }
135
+
136
+ const content = await response.text();
137
+ const parsed = parseYamlFrontmatter(content);
138
+
139
+ return {
140
+ name: parsed.name || skill.name,
141
+ description: parsed.description || "(no description)",
142
+ category: parsed.category,
143
+ author: parsed.author,
144
+ version: parsed.version,
145
+ tags: parsed.tags,
146
+ };
147
+ }
148
+
149
+ // ─── Check installation ───────────────────────────────────────────────────────
150
+
151
+ export async function getInstalledSkillNames(
152
+ scope: "user" | "project",
153
+ projectPath?: string,
154
+ ): Promise<Set<string>> {
155
+ const dir =
156
+ scope === "user"
157
+ ? getUserSkillsDir()
158
+ : getProjectSkillsDir(projectPath);
159
+
160
+ const installed = new Set<string>();
161
+
162
+ try {
163
+ if (!(await fs.pathExists(dir))) return installed;
164
+ const entries = await fs.readdir(dir);
165
+ for (const entry of entries) {
166
+ const skillMd = path.join(dir, entry, "SKILL.md");
167
+ if (await fs.pathExists(skillMd)) {
168
+ installed.add(entry);
169
+ }
170
+ }
171
+ } catch {
172
+ // ignore
173
+ }
174
+
175
+ return installed;
176
+ }
177
+
178
+ // ─── Fetch available skills ───────────────────────────────────────────────────
179
+
180
+ export async function fetchAvailableSkills(
181
+ repos: SkillSource[],
182
+ projectPath?: string,
183
+ ): Promise<SkillInfo[]> {
184
+ const userInstalled = await getInstalledSkillNames("user");
185
+ const projectInstalled = await getInstalledSkillNames("project", projectPath);
186
+
187
+ const skills: SkillInfo[] = [];
188
+
189
+ for (const source of repos) {
190
+ let tree: GitTreeResponse;
191
+ try {
192
+ tree = await fetchGitTree(source.repo);
193
+ } catch {
194
+ // Skip this repo on error
195
+ continue;
196
+ }
197
+
198
+ // Filter for SKILL.md files under skillsPath
199
+ const prefix = source.skillsPath ? `${source.skillsPath}/` : "";
200
+
201
+ for (const item of tree.tree) {
202
+ if (item.type !== "blob") continue;
203
+ if (!item.path.endsWith("/SKILL.md")) continue;
204
+ if (prefix && !item.path.startsWith(prefix)) continue;
205
+
206
+ // Extract skill name: second-to-last segment of path
207
+ const parts = item.path.split("/");
208
+ if (parts.length < 2) continue;
209
+ const skillName = parts[parts.length - 2];
210
+
211
+ // Validate name (prevent traversal)
212
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(skillName)) continue;
213
+
214
+ const isUserInstalled = userInstalled.has(skillName);
215
+ const isProjInstalled = projectInstalled.has(skillName);
216
+ const installed = isUserInstalled || isProjInstalled;
217
+ const installedScope: "user" | "project" | null = isProjInstalled
218
+ ? "project"
219
+ : isUserInstalled
220
+ ? "user"
221
+ : null;
222
+
223
+ skills.push({
224
+ id: `${source.repo}/${item.path}`,
225
+ name: skillName,
226
+ source,
227
+ repoPath: item.path,
228
+ gitBlobSha: item.sha,
229
+ frontmatter: null,
230
+ installed,
231
+ installedScope,
232
+ hasUpdate: false,
233
+ });
234
+ }
235
+ }
236
+
237
+ // Sort by name within each repo
238
+ skills.sort((a, b) => a.name.localeCompare(b.name));
239
+
240
+ return skills;
241
+ }
242
+
243
+ // ─── Install / Uninstall ──────────────────────────────────────────────────────
244
+
245
+ export async function installSkill(
246
+ skill: SkillInfo,
247
+ scope: "user" | "project",
248
+ projectPath?: string,
249
+ ): Promise<void> {
250
+ const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
251
+
252
+ const response = await fetch(url, {
253
+ signal: AbortSignal.timeout(15000),
254
+ });
255
+
256
+ if (!response.ok) {
257
+ throw new Error(
258
+ `Failed to fetch skill: ${response.status} ${response.statusText}`,
259
+ );
260
+ }
261
+
262
+ const content = await response.text();
263
+
264
+ const installDir =
265
+ scope === "user"
266
+ ? path.join(getUserSkillsDir(), skill.name)
267
+ : path.join(getProjectSkillsDir(projectPath), skill.name);
268
+
269
+ await fs.ensureDir(installDir);
270
+ await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
271
+ }
272
+
273
+ export async function uninstallSkill(
274
+ skillName: string,
275
+ scope: "user" | "project",
276
+ projectPath?: string,
277
+ ): Promise<void> {
278
+ const installDir =
279
+ scope === "user"
280
+ ? path.join(getUserSkillsDir(), skillName)
281
+ : path.join(getProjectSkillsDir(projectPath), skillName);
282
+
283
+ const skillMdPath = path.join(installDir, "SKILL.md");
284
+
285
+ if (await fs.pathExists(skillMdPath)) {
286
+ await fs.remove(skillMdPath);
287
+ }
288
+
289
+ // Try to remove directory if empty
290
+ try {
291
+ await fs.rmdir(installDir);
292
+ } catch {
293
+ // Ignore if not empty or doesn't exist
294
+ }
295
+ }
296
+
297
+ // ─── Check installed skills from file system ──────────────────────────────────
298
+
299
+ export async function getInstalledSkillsFromFs(
300
+ projectPath?: string,
301
+ ): Promise<{ name: string; scope: "user" | "project" }[]> {
302
+ const result: { name: string; scope: "user" | "project" }[] = [];
303
+
304
+ const userDir = getUserSkillsDir();
305
+ const projDir = getProjectSkillsDir(projectPath);
306
+
307
+ const scopedDirs: Array<[string, "user" | "project"]> = [
308
+ [userDir, "user"],
309
+ [projDir, "project"],
310
+ ];
311
+
312
+ for (const [dir, scope] of scopedDirs) {
313
+ try {
314
+ if (!(await fs.pathExists(dir))) continue;
315
+ const entries = await fs.readdir(dir);
316
+ for (const entry of entries) {
317
+ const skillMd = path.join(dir, entry, "SKILL.md");
318
+ if (await fs.pathExists(skillMd)) {
319
+ result.push({ name: entry, scope });
320
+ }
321
+ }
322
+ } catch {
323
+ // ignore
324
+ }
325
+ }
326
+
327
+ return result;
328
+ }
@@ -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,73 @@ 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
+ }
168
+
169
+ // ─── GitHub Tree API types ────────────────────────────────────────────────────
170
+
171
+ export interface GitTreeItem {
172
+ path: string;
173
+ mode: string;
174
+ type: "blob" | "tree";
175
+ sha: string;
176
+ size?: number;
177
+ url: string;
178
+ }
179
+
180
+ export interface GitTreeResponse {
181
+ sha: string;
182
+ url: string;
183
+ tree: GitTreeItem[];
184
+ truncated: boolean;
185
+ }
120
186
 
121
187
  // MCP Registry Types (registry.modelcontextprotocol.io)
122
188
  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,6 +86,7 @@ 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")
@@ -96,6 +99,8 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
96
99
  navigateToScreen("cli-tools");
97
100
  else if (input === "5")
98
101
  navigateToScreen("profiles");
102
+ else if (input === "6")
103
+ navigateToScreen("skills");
99
104
  // Tab navigation cycling
100
105
  if (key.tab) {
101
106
  const screens = [
@@ -104,6 +109,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
104
109
  "settings",
105
110
  "cli-tools",
106
111
  "profiles",
112
+ "skills",
107
113
  ];
108
114
  const currentIndex = screens.indexOf(state.currentRoute.screen);
109
115
  if (currentIndex !== -1) {
@@ -140,7 +146,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
140
146
  Quick Navigation
141
147
  1 Plugins 4 CLI Tools
142
148
  2 MCP Servers 5 Profiles
143
- 3 Settings
149
+ 3 Settings 6 Skills
144
150
 
145
151
  Plugin Actions
146
152
  u Update d Uninstall
package/src/ui/App.tsx CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  CliToolsScreen,
21
21
  ModelSelectorScreen,
22
22
  ProfilesScreen,
23
+ SkillsScreen,
23
24
  } from "./screens/index.js";
24
25
  import type { Screen } from "./state/types.js";
25
26
  import { repairAllMarketplaces } from "../services/local-marketplace.js";
@@ -57,6 +58,8 @@ function Router() {
57
58
  return <ModelSelectorScreen />;
58
59
  case "profiles":
59
60
  return <ProfilesScreen />;
61
+ case "skills":
62
+ return <SkillsScreen />;
60
63
  default:
61
64
  return <PluginsScreen />;
62
65
  }
@@ -116,6 +119,7 @@ function GlobalKeyHandler({
116
119
  "cli-tools",
117
120
  "model-selector",
118
121
  "profiles",
122
+ "skills",
119
123
  ].includes(state.currentRoute.screen);
120
124
 
121
125
  if (isTopLevel) {
@@ -124,6 +128,7 @@ function GlobalKeyHandler({
124
128
  else if (input === "3") navigateToScreen("settings");
125
129
  else if (input === "4") navigateToScreen("cli-tools");
126
130
  else if (input === "5") navigateToScreen("profiles");
131
+ else if (input === "6") navigateToScreen("skills");
127
132
 
128
133
  // Tab navigation cycling
129
134
  if (key.tab) {
@@ -133,6 +138,7 @@ function GlobalKeyHandler({
133
138
  "settings",
134
139
  "cli-tools",
135
140
  "profiles",
141
+ "skills",
136
142
  ];
137
143
  const currentIndex = screens.indexOf(
138
144
  state.currentRoute.screen as Screen,
@@ -173,7 +179,7 @@ function GlobalKeyHandler({
173
179
  Quick Navigation
174
180
  1 Plugins 4 CLI Tools
175
181
  2 MCP Servers 5 Profiles
176
- 3 Settings
182
+ 3 Settings 6 Skills
177
183
 
178
184
  Plugin Actions
179
185
  u Update d Uninstall
@@ -6,6 +6,7 @@ const TABS = [
6
6
  { key: "3", label: "Settings", screen: "settings" },
7
7
  { key: "4", label: "CLI", screen: "cli-tools" },
8
8
  { key: "5", label: "Profiles", screen: "profiles" },
9
+ { key: "6", label: "Skills", screen: "skills" },
9
10
  ];
10
11
  export function TabBar({ currentScreen, onTabChange }) {
11
12
  // Handle number key shortcuts (1-5)
@@ -14,6 +14,7 @@ const TABS: Tab[] = [
14
14
  { key: "3", label: "Settings", screen: "settings" },
15
15
  { key: "4", label: "CLI", screen: "cli-tools" },
16
16
  { key: "5", label: "Profiles", screen: "profiles" },
17
+ { key: "6", label: "Skills", screen: "skills" },
17
18
  ];
18
19
 
19
20
  interface TabBarProps {