@skillsgate/tui 0.1.10 → 0.1.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillsgate/tui",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillsgate-tui": "bin/skillsgate-tui"
@@ -15,13 +15,6 @@
15
15
  "@opentui/react": "0.1.90",
16
16
  "gray-matter": "4.0.3"
17
17
  },
18
- "optionalDependencies": {
19
- "@skillsgate/tui-darwin-arm64": "0.1.10",
20
- "@skillsgate/tui-darwin-x64": "0.1.10",
21
- "@skillsgate/tui-linux-x64": "0.1.10",
22
- "@skillsgate/tui-linux-arm64": "0.1.10",
23
- "@skillsgate/tui-win32-x64": "0.1.10"
24
- },
25
18
  "peerDependencies": {
26
19
  "react": "19.1.5"
27
20
  },
@@ -58,11 +58,11 @@ export function AgentFilter() {
58
58
  style={{
59
59
  flexDirection: "column",
60
60
  width: 22,
61
- borderRight: true,
61
+ border: true,
62
62
  borderColor: isFocused ? colors.primary : colors.border,
63
63
  backgroundColor: colors.bg,
64
64
  paddingTop: 0,
65
- }}
65
+ } as any}
66
66
  >
67
67
  {/* Library section header */}
68
68
  <box style={{ paddingLeft: 1, height: 1, backgroundColor: colors.bgAlt }}>
@@ -10,20 +10,20 @@ const SHORTCUTS_LEFT: ShortcutEntry[] = [
10
10
  { key: "g", description: "Jump to first item" },
11
11
  { key: "G", description: "Jump to last item" },
12
12
  { key: "v", description: "View skill detail" },
13
+ { key: "n", description: "Create local skill (home)" },
14
+ { key: "c", description: "Manage collections (home)" },
13
15
  { key: "/", description: "Focus search input" },
14
16
  { key: "Tab", description: "Cycle focus: agents > search > list" },
15
17
  { key: "Esc", description: "Clear search / go back" },
16
18
  ]
17
19
 
18
20
  const SHORTCUTS_RIGHT: ShortcutEntry[] = [
19
- { key: "1/2/3/4", description: "Switch tabs" },
21
+ { key: "1/2/3", description: "Switch tabs" },
20
22
  { key: "s", description: "Open settings" },
21
23
  { key: "r", description: "Refresh installed skills" },
22
- { key: "l", description: "Login / auth status" },
23
24
  { key: "i", description: "Install selected skill" },
24
25
  { key: "d", description: "Remove selected skill" },
25
- { key: "x", description: "Unfavorite (favorites view)" },
26
- { key: "m", description: "Toggle keyword/AI search" },
26
+ { key: "", description: "" },
27
27
  { key: "?", description: "Toggle this help" },
28
28
  { key: "Ctrl+Q", description: "Quit" },
29
29
  { key: "", description: "" },
@@ -4,30 +4,22 @@ import { useStore, useDispatch } from "../store/context.js"
4
4
  import { useDb } from "../db/context.js"
5
5
  import { useDetectedAgents } from "../data/use-agents.js"
6
6
  import { useInstalledSkills } from "../data/use-installed-skills.js"
7
- import { useAuth } from "../data/use-auth.js"
8
7
  import { StatusBar } from "./status-bar.js"
9
8
  import { HelpOverlay } from "./help-overlay.js"
10
9
  import { HomeView } from "../views/home.js"
11
10
  import { SkillDetailView } from "../views/skill-detail.js"
12
11
  import { DiscoverView } from "../views/discover.js"
13
- import { FavoritesView } from "../views/favorites.js"
14
12
  import { ServersView } from "../views/servers.js"
15
13
  import { AddServerView } from "../views/add-server.js"
16
14
  import { ServerSkillsView } from "../views/server-skills.js"
17
15
  import { SettingsView } from "../views/settings.js"
18
- import { LoginView } from "../views/login.js"
19
16
  import { colors } from "../utils/colors.js"
20
17
  import type { ViewName } from "../store/types.js"
21
18
 
22
- function getTabOptions(favCount: number, serverCount: number) {
19
+ function getTabOptions(serverCount: number) {
23
20
  return [
24
21
  { name: "Installed", description: "Locally installed skills", value: "home" },
25
22
  { name: "Discover", description: "Search the registry", value: "discover" },
26
- {
27
- name: favCount > 0 ? `Favorites (${favCount})` : "Favorites",
28
- description: "Your starred skills",
29
- value: "favorites",
30
- },
31
23
  {
32
24
  name: serverCount > 0 ? `Servers (${serverCount})` : "Servers",
33
25
  description: "Remote SSH servers",
@@ -43,8 +35,7 @@ export function Layout() {
43
35
  const { servers } = useDb()
44
36
  const [serverCount, setServerCount] = useState(() => servers.list().length)
45
37
 
46
- // Load auth, agent + skill data on mount
47
- useAuth()
38
+ // Load agent + skill data on mount
48
39
  useDetectedAgents()
49
40
  useInstalledSkills()
50
41
 
@@ -71,14 +62,6 @@ export function Layout() {
71
62
  return
72
63
  }
73
64
 
74
- // When on login view, only handle Escape -- let input keys pass through
75
- if (state.activeView === "login") {
76
- if (key.name === "escape") {
77
- dispatch({ type: "GO_BACK" })
78
- }
79
- return
80
- }
81
-
82
65
  // Help overlay toggle
83
66
  if (key.name === "?" || (key.shift && key.name === "/")) {
84
67
  dispatch({ type: "TOGGLE_HELP" })
@@ -95,21 +78,21 @@ export function Layout() {
95
78
  if (state.showHelp) return
96
79
 
97
80
  // Tab switching (only when not in detail/form views)
98
- const inFormView = state.activeView === "detail" || state.activeView === "add-server"
99
- || state.activeView === "edit-server" || state.activeView === "settings"
100
- || state.activeView === "server-skills" || state.activeView === "login"
81
+ const activeView = state.activeView as string
82
+ const inFormView = activeView === "detail" || activeView === "add-server"
83
+ || activeView === "edit-server" || activeView === "settings"
84
+ || activeView === "server-skills"
101
85
  if (!inFormView) {
102
86
  if (key.name === "1") dispatch({ type: "NAVIGATE", view: "home" })
103
87
  if (key.name === "2") dispatch({ type: "NAVIGATE", view: "discover" })
104
- if (key.name === "3") dispatch({ type: "NAVIGATE", view: "favorites" })
105
- if (key.name === "4") {
88
+ if (key.name === "3") {
106
89
  setServerCount(servers.list().length)
107
90
  dispatch({ type: "NAVIGATE", view: "servers" })
108
91
  }
109
92
  }
110
93
 
111
- // "s" to open settings (only from home/favorites views when not in search)
112
- if (key.name === "s" && state.focusedPane !== "search"
94
+ // "s" to open settings (only from home/servers views when not in search)
95
+ if (key.name === "s" && (state.focusedPane as string) !== "search"
113
96
  && state.activeView !== "discover" && state.activeView !== "detail"
114
97
  && !inFormView) {
115
98
  dispatch({ type: "NAVIGATE", view: "settings" })
@@ -139,26 +122,20 @@ export function Layout() {
139
122
  if (state.installedFilter) {
140
123
  dispatch({ type: "SET_INSTALLED_FILTER", filter: "" })
141
124
  }
142
- if (state.focusedPane === "search") {
125
+ if ((state.focusedPane as string) === "search") {
143
126
  dispatch({ type: "SET_FOCUSED_PANE", pane: "list" })
144
127
  }
145
128
  return
146
129
  }
147
130
 
148
- // "l" to navigate to login view (always -- allows re-login if token expired)
149
- if (key.name === "l" && state.focusedPane !== "search" && state.activeView !== "detail" && state.activeView !== "login") {
150
- dispatch({ type: "NAVIGATE", view: "login" })
151
- return
152
- }
153
-
154
- // "r" to refresh installed skills (when not typing in search, not on login view)
155
- if (key.name === "r" && state.focusedPane !== "search" && state.activeView !== "detail" && state.activeView !== "login") {
131
+ // "r" to refresh installed skills (when not typing in search)
132
+ if (key.name === "r" && (state.focusedPane as string) !== "search" && activeView !== "detail") {
156
133
  dispatch({ type: "REFRESH_SKILLS" })
157
134
  return
158
135
  }
159
136
  })
160
137
 
161
- const TAB_OPTIONS = getTabOptions(state.favorites.length, serverCount)
138
+ const TAB_OPTIONS = getTabOptions(serverCount)
162
139
 
163
140
  const activeTabIndex = TAB_OPTIONS.findIndex(
164
141
  (t) => t.value === state.activeView
@@ -186,7 +163,7 @@ export function Layout() {
186
163
  }}
187
164
  >
188
165
  <text fg={colors.primary}>
189
- <strong>SkillsGate TUI</strong> <span fg={colors.textDim}>v0.1.0</span>
166
+ <strong>SkillsGate TUI</strong> <span fg={colors.textDim}>v0.1.12</span>
190
167
  </text>
191
168
  </box>
192
169
 
@@ -194,7 +171,7 @@ export function Layout() {
194
171
  <tab-select
195
172
  options={TAB_OPTIONS}
196
173
  focused={state.activeView !== "detail" && !state.showHelp}
197
- selectedIndex={activeTabIndex >= 0 ? activeTabIndex : 0}
174
+ {...({ selectedIndex: activeTabIndex >= 0 ? activeTabIndex : 0 } as any)}
198
175
  selectedBackgroundColor={colors.tabActive}
199
176
  selectedTextColor={colors.tabText}
200
177
  textColor={colors.textDim}
@@ -216,7 +193,6 @@ export function Layout() {
216
193
  <>
217
194
  {state.activeView === "home" && <HomeView />}
218
195
  {state.activeView === "discover" && <DiscoverView />}
219
- {state.activeView === "favorites" && <FavoritesView />}
220
196
  {state.activeView === "servers" && <ServersView onServerCountChange={setServerCount} />}
221
197
  {(state.activeView === "add-server" || state.activeView === "edit-server") && (
222
198
  <AddServerView
@@ -228,7 +204,6 @@ export function Layout() {
228
204
  <ServerSkillsView serverId={state.selectedServerId} />
229
205
  )}
230
206
  {state.activeView === "settings" && <SettingsView />}
231
- {state.activeView === "login" && <LoginView />}
232
207
  {state.activeView === "detail" && state.selectedSkill && (
233
208
  <SkillDetailView />
234
209
  )}
@@ -6,16 +6,13 @@ export function StatusBar() {
6
6
 
7
7
  const skillCount = state.installedSkills.length
8
8
  const agentCount = state.detectedAgents.length
9
- const user = state.auth?.user?.name ?? "not logged in (l=login)"
10
9
  const focusHint = state.activeView === "detail"
11
10
  ? "q=back"
12
- : state.activeView === "login"
13
- ? "Esc=back"
14
- : state.focusedPane === "search"
15
- ? "Tab=results Esc=exit search"
16
- : "/=search Tab=switch pane"
11
+ : state.focusedPane === "search"
12
+ ? "Tab=results Esc=exit search"
13
+ : "/=search Tab=switch pane"
17
14
 
18
- const statusText = `Skills: ${skillCount} | Agents: ${agentCount} | ${user} | ${focusHint} | ?=help 1/2/3/4=tabs`
15
+ const statusText = `Skills: ${skillCount} | Agents: ${agentCount} | ${focusHint} | ?=help 1/2/3=tabs`
19
16
 
20
17
  return (
21
18
  <box
@@ -1,151 +1,120 @@
1
- const API_BASE = process.env.SKILLSGATE_SEARCH_API_URL ?? "https://api.skillsgate.ai"
1
+ const SKILLSGATE_API_BASE = process.env.SKILLSGATE_SEARCH_API_URL ?? "https://api.skillsgate.ai"
2
+ const SKILLS_SH_BASE = "https://skills.sh"
3
+ const GITHUB_API_BASE = "https://api.github.com"
4
+ const GITHUB_RAW_BASE = "https://raw.githubusercontent.com"
2
5
 
3
6
  // ---------- Types ----------
4
7
 
5
8
  export interface CatalogSkill {
6
9
  id: string
7
- slug: string
10
+ skillId: string
8
11
  name: string
9
- description: string
10
- summary?: string
11
- categories: string[]
12
- capabilities?: string[]
13
- keywords?: string[]
14
- githubUrl?: string
15
- githubStars?: number | null
16
- installCommand?: string | null
17
- score?: number
18
- username?: string
19
- urlPath?: string
12
+ installs: number
13
+ source: string
20
14
  }
21
15
 
22
- interface CatalogResponse {
16
+ interface SkillsShSearchResponse {
23
17
  skills: CatalogSkill[]
24
- meta: {
25
- total: number
26
- limit: number
27
- offset: number
28
- hasMore?: boolean
29
- }
30
- }
31
-
32
- interface KeywordSearchResponse {
33
- skills: CatalogSkill[]
34
- meta: {
35
- total: number
36
- limit: number
37
- offset: number
38
- hasMore?: boolean
39
- }
18
+ count: number
40
19
  }
41
20
 
42
- interface SemanticSearchResponse {
43
- results: CatalogSkill[]
44
- meta: {
45
- query: string
46
- total: number
47
- limit: number
48
- remainingSearches: number
49
- }
50
- }
51
-
52
- export type SearchMode = "keyword" | "semantic"
53
-
54
21
  export interface SearchResult {
55
22
  skills: CatalogSkill[]
56
23
  total: number
57
- remainingSearches?: number
58
- }
59
-
60
- // ---------- Catalog ----------
61
-
62
- export async function fetchCatalog(
63
- limit: number = 20,
64
- offset: number = 0
65
- ): Promise<{ skills: CatalogSkill[]; total: number }> {
66
- const url = `${API_BASE}/api/v1/skills?limit=${limit}&offset=${offset}`
67
-
68
- const response = await fetch(url, {
69
- headers: { "Content-Type": "application/json" },
70
- })
71
-
72
- if (!response.ok) {
73
- throw new Error(`Catalog fetch failed (HTTP ${response.status})`)
74
- }
75
-
76
- const data = (await response.json()) as CatalogResponse
77
- return { skills: data.skills ?? [], total: data.meta?.total ?? 0 }
78
24
  }
79
25
 
80
- // ---------- Keyword Search (public, no auth) ----------
26
+ // ---------- Search (skills.sh) ----------
81
27
 
82
- export async function keywordSearch(
28
+ /**
29
+ * Search for skills or load popular skills when query is empty.
30
+ * Public endpoint, no authentication required.
31
+ */
32
+ export async function searchSkills(
83
33
  query: string,
84
34
  limit: number = 20,
85
35
  offset: number = 0
86
36
  ): Promise<SearchResult> {
87
- const url = `${API_BASE}/api/v1/skills/search?q=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}`
37
+ // skills.sh requires a minimum 2-char query
38
+ const q = query.trim() || "skill"
39
+ const params = new URLSearchParams({ q, limit: String(limit), offset: String(offset) })
88
40
 
89
- const response = await fetch(url, {
90
- headers: { "Content-Type": "application/json" },
91
- })
41
+ const url = `${SKILLS_SH_BASE}/api/search?${params}`
42
+ const response = await fetch(url)
92
43
 
93
44
  if (!response.ok) {
94
- throw new Error(`Keyword search failed (HTTP ${response.status})`)
45
+ throw new Error(`Search failed (HTTP ${response.status})`)
95
46
  }
96
47
 
97
- const data = (await response.json()) as KeywordSearchResponse
48
+ const data = (await response.json()) as SkillsShSearchResponse
98
49
  return {
99
50
  skills: data.skills ?? [],
100
- total: data.meta?.total ?? 0,
51
+ total: data.count ?? 0,
101
52
  }
102
53
  }
103
54
 
104
- // ---------- Semantic Search (authenticated, rate limited) ----------
55
+ // ---------- Skill Content (GitHub raw) ----------
105
56
 
106
- export async function semanticSearch(
107
- query: string,
108
- token: string,
109
- limit: number = 5
110
- ): Promise<SearchResult> {
111
- const url = `${API_BASE}/api/v1/search`
112
-
113
- const response = await fetch(url, {
114
- method: "POST",
115
- headers: {
116
- "Content-Type": "application/json",
117
- Authorization: `Bearer ${token}`,
118
- },
119
- body: JSON.stringify({ query, limit }),
57
+ /**
58
+ * In-memory cache for GitHub default branch lookups.
59
+ */
60
+ const branchCache = new Map<string, string>()
61
+
62
+ /**
63
+ * Fetches the default branch for a GitHub repository.
64
+ */
65
+ async function getDefaultBranch(source: string): Promise<string> {
66
+ const cached = branchCache.get(source)
67
+ if (cached) return cached
68
+
69
+ const response = await fetch(`${GITHUB_API_BASE}/repos/${source}`, {
70
+ headers: { Accept: "application/vnd.github.v3+json" },
120
71
  })
121
72
 
122
73
  if (!response.ok) {
123
- if (response.status === 429) {
124
- throw new Error("RATE_LIMIT")
125
- }
126
- if (response.status === 401) {
127
- throw new Error("AUTH_EXPIRED")
128
- }
129
- throw new Error(`Semantic search failed (HTTP ${response.status})`)
74
+ throw new Error(`GitHub API error (HTTP ${response.status})`)
130
75
  }
131
76
 
132
- const data = (await response.json()) as SemanticSearchResponse
133
- return {
134
- skills: data.results ?? [],
135
- total: data.meta?.total ?? 0,
136
- remainingSearches: data.meta?.remainingSearches,
137
- }
77
+ const data = (await response.json()) as { default_branch: string }
78
+ const branch = data.default_branch
79
+ branchCache.set(source, branch)
80
+ return branch
138
81
  }
139
82
 
140
- // ---------- Unified search ----------
83
+ /**
84
+ * Candidate paths where a SKILL.md file might be located in a repository.
85
+ */
86
+ function candidatePaths(skillId: string): string[] {
87
+ return [
88
+ `skills/${skillId}/SKILL.md`,
89
+ `skills/.curated/${skillId}/SKILL.md`,
90
+ `skills/.experimental/${skillId}/SKILL.md`,
91
+ `${skillId}/SKILL.md`,
92
+ `SKILL.md`,
93
+ ]
94
+ }
141
95
 
142
- export async function searchSkills(
143
- query: string,
144
- mode: SearchMode,
145
- token?: string | null
146
- ): Promise<SearchResult> {
147
- if (mode === "semantic" && token) {
148
- return semanticSearch(query, token)
96
+ /**
97
+ * Fetches the SKILL.md content for a skill by trying multiple path candidates
98
+ * against the GitHub raw content API.
99
+ */
100
+ export async function fetchSkillContent(
101
+ source: string,
102
+ skillId: string
103
+ ): Promise<string | null> {
104
+ const branch = await getDefaultBranch(source)
105
+ const paths = candidatePaths(skillId)
106
+
107
+ for (const p of paths) {
108
+ const url = `${GITHUB_RAW_BASE}/${source}/${branch}/${p}`
109
+ const response = await fetch(url)
110
+ if (response.ok) {
111
+ return response.text()
112
+ }
149
113
  }
150
- return keywordSearch(query)
114
+
115
+ return null
151
116
  }
117
+
118
+ // ---------- SkillsGate API (auth, favorites, private skills) ----------
119
+
120
+ export { SKILLSGATE_API_BASE }
@@ -1,13 +1,27 @@
1
1
  import { useState, useEffect, useCallback } from "react"
2
2
  import { useStore, useDispatch } from "../store/context.js"
3
- import type { CatalogSkill } from "./api-client.js"
3
+ import { SKILLSGATE_API_BASE } from "./api-client.js"
4
4
 
5
- const API_BASE = process.env.SKILLSGATE_SEARCH_API_URL ?? "https://api.skillsgate.ai"
6
-
7
- interface FavoriteSkill extends CatalogSkill {
5
+ /**
6
+ * Shape returned by the favorites API endpoint.
7
+ */
8
+ export interface FavoriteSkill {
9
+ id: string
10
+ name: string
11
+ description: string
12
+ summary?: string
13
+ categories: string[]
14
+ capabilities?: string[]
15
+ keywords?: string[]
16
+ githubUrl?: string
17
+ githubStars?: number | null
18
+ installCommand?: string | null
19
+ slug?: string
8
20
  favoriteId?: string
9
21
  }
10
22
 
23
+ const API_BASE = SKILLSGATE_API_BASE
24
+
11
25
  interface UseFavoritesResult {
12
26
  favorites: FavoriteSkill[]
13
27
  loading: boolean