@skillsgate/tui 0.1.12 → 0.1.14

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.12",
3
+ "version": "0.1.14",
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.12",
20
- "@skillsgate/tui-darwin-x64": "0.1.12",
21
- "@skillsgate/tui-linux-x64": "0.1.12",
22
- "@skillsgate/tui-linux-arm64": "0.1.12",
23
- "@skillsgate/tui-win32-x64": "0.1.12"
24
- },
25
18
  "peerDependencies": {
26
19
  "react": "19.1.5"
27
20
  },
@@ -18,14 +18,12 @@ const SHORTCUTS_LEFT: ShortcutEntry[] = [
18
18
  ]
19
19
 
20
20
  const SHORTCUTS_RIGHT: ShortcutEntry[] = [
21
- { key: "1/2/3/4", description: "Switch tabs" },
21
+ { key: "1/2/3", description: "Switch tabs" },
22
22
  { key: "s", description: "Open settings" },
23
23
  { key: "r", description: "Refresh installed skills" },
24
- { key: "l", description: "Login / auth status" },
25
24
  { key: "i", description: "Install selected skill" },
26
25
  { key: "d", description: "Remove selected skill" },
27
- { key: "x", description: "Unfavorite (favorites view)" },
28
- { key: "m", description: "Toggle keyword/AI search" },
26
+ { key: "", description: "" },
29
27
  { key: "?", description: "Toggle this help" },
30
28
  { key: "Ctrl+Q", description: "Quit" },
31
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" })
@@ -98,18 +81,17 @@ export function Layout() {
98
81
  const activeView = state.activeView as string
99
82
  const inFormView = activeView === "detail" || activeView === "add-server"
100
83
  || activeView === "edit-server" || activeView === "settings"
101
- || activeView === "server-skills" || activeView === "login"
84
+ || activeView === "server-skills"
102
85
  if (!inFormView) {
103
86
  if (key.name === "1") dispatch({ type: "NAVIGATE", view: "home" })
104
87
  if (key.name === "2") dispatch({ type: "NAVIGATE", view: "discover" })
105
- if (key.name === "3") dispatch({ type: "NAVIGATE", view: "favorites" })
106
- if (key.name === "4") {
88
+ if (key.name === "3") {
107
89
  setServerCount(servers.list().length)
108
90
  dispatch({ type: "NAVIGATE", view: "servers" })
109
91
  }
110
92
  }
111
93
 
112
- // "s" to open settings (only from home/favorites views when not in search)
94
+ // "s" to open settings (only from home/servers views when not in search)
113
95
  if (key.name === "s" && (state.focusedPane as string) !== "search"
114
96
  && state.activeView !== "discover" && state.activeView !== "detail"
115
97
  && !inFormView) {
@@ -146,20 +128,14 @@ export function Layout() {
146
128
  return
147
129
  }
148
130
 
149
- // "l" to navigate to login view (always -- allows re-login if token expired)
150
- if (key.name === "l" && (state.focusedPane as string) !== "search" && activeView !== "detail" && activeView !== "login") {
151
- dispatch({ type: "NAVIGATE", view: "login" })
152
- return
153
- }
154
-
155
- // "r" to refresh installed skills (when not typing in search, not on login view)
156
- if (key.name === "r" && (state.focusedPane as string) !== "search" && activeView !== "detail" && 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") {
157
133
  dispatch({ type: "REFRESH_SKILLS" })
158
134
  return
159
135
  }
160
136
  })
161
137
 
162
- const TAB_OPTIONS = getTabOptions(state.favorites.length, serverCount)
138
+ const TAB_OPTIONS = getTabOptions(serverCount)
163
139
 
164
140
  const activeTabIndex = TAB_OPTIONS.findIndex(
165
141
  (t) => t.value === state.activeView
@@ -217,7 +193,6 @@ export function Layout() {
217
193
  <>
218
194
  {state.activeView === "home" && <HomeView />}
219
195
  {state.activeView === "discover" && <DiscoverView />}
220
- {state.activeView === "favorites" && <FavoritesView />}
221
196
  {state.activeView === "servers" && <ServersView onServerCountChange={setServerCount} />}
222
197
  {(state.activeView === "add-server" || state.activeView === "edit-server") && (
223
198
  <AddServerView
@@ -229,7 +204,6 @@ export function Layout() {
229
204
  <ServerSkillsView serverId={state.selectedServerId} />
230
205
  )}
231
206
  {state.activeView === "settings" && <SettingsView />}
232
- {state.activeView === "login" && <LoginView />}
233
207
  {state.activeView === "detail" && state.selectedSkill && (
234
208
  <SkillDetailView />
235
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