@skillsgate/tui 0.1.12 → 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 +1 -8
- package/src/components/help-overlay.tsx +2 -4
- package/src/components/layout.tsx +8 -34
- package/src/components/status-bar.tsx +4 -7
- package/src/data/api-client.ts +78 -109
- package/src/data/use-favorites.ts +18 -4
- package/src/data/use-search.ts +14 -31
- package/src/data/use-skill-actions.ts +68 -16
- package/src/views/discover.tsx +33 -126
- package/src/views/favorites.tsx +10 -354
- package/src/views/settings.tsx +0 -6
- package/src/views/skill-detail.tsx +15 -14
- package/tmp.json +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useCallback } from "react"
|
|
2
|
+
import { exec as execCb } from "node:child_process"
|
|
3
|
+
import { promisify } from "node:util"
|
|
2
4
|
import { useStore, useDispatch } from "../store/context.js"
|
|
3
5
|
import { useDb } from "../db/context.js"
|
|
4
|
-
import type { EnrichedSkill } from "../store/types.js"
|
|
6
|
+
import type { EnrichedSkill, Action } from "../store/types.js"
|
|
5
7
|
|
|
6
8
|
// CLI core imports -- these share the same Bun runtime
|
|
7
9
|
import { parseSource } from "../../../cli/src/core/source-parser.js"
|
|
8
|
-
import {
|
|
10
|
+
import { cleanupTempDir, fetchTreeSha } from "../../../cli/src/core/git.js"
|
|
9
11
|
import { discoverSkills } from "../../../cli/src/core/skill-discovery.js"
|
|
10
12
|
import {
|
|
11
13
|
installSkillForAgent,
|
|
@@ -16,12 +18,11 @@ import {
|
|
|
16
18
|
import {
|
|
17
19
|
addSkillToLock,
|
|
18
20
|
removeSkillFromLock,
|
|
19
|
-
readSkillLock,
|
|
20
21
|
} from "../../../cli/src/core/skill-lock.js"
|
|
21
22
|
import { agents, detectInstalledAgents } from "../../../cli/src/core/agents.js"
|
|
22
23
|
import { downloadSkill } from "../../../cli/src/core/skillsgate-client.js"
|
|
23
24
|
import { getToken } from "../../../cli/src/utils/auth-store.js"
|
|
24
|
-
import type { Skill, AgentConfig
|
|
25
|
+
import type { Skill, AgentConfig } from "../../../cli/src/types.js"
|
|
25
26
|
|
|
26
27
|
interface UseSkillActionsResult {
|
|
27
28
|
installSkill: (skill: EnrichedSkill) => Promise<void>
|
|
@@ -40,7 +41,9 @@ export function useSkillActions(): UseSkillActionsResult {
|
|
|
40
41
|
const { settings } = useDb()
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
* Install a skill from its source
|
|
44
|
+
* Install a skill from its source.
|
|
45
|
+
* For public skills (with a source in owner/repo format), runs `npx skills add`.
|
|
46
|
+
* For private skills, uses the existing download flow.
|
|
44
47
|
*/
|
|
45
48
|
const installSkill = useCallback(async (skill: EnrichedSkill) => {
|
|
46
49
|
dispatch({
|
|
@@ -61,7 +64,16 @@ export function useSkillActions(): UseSkillActionsResult {
|
|
|
61
64
|
|
|
62
65
|
const source = parseSource(sourceStr)
|
|
63
66
|
|
|
64
|
-
//
|
|
67
|
+
// Public skills (owner/repo format): use `npx skills add`
|
|
68
|
+
if (source.type === "github" || isOwnerRepoFormat(sourceStr)) {
|
|
69
|
+
const repo = source.type === "github"
|
|
70
|
+
? `${source.owner}/${source.repo}`
|
|
71
|
+
: sourceStr
|
|
72
|
+
await runSkillsAdd(repo, dispatch)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Private skills: use the existing download flow
|
|
65
77
|
const installedAgents = await detectInstalledAgents()
|
|
66
78
|
if (installedAgents.length === 0) {
|
|
67
79
|
dispatch({
|
|
@@ -88,19 +100,16 @@ export function useSkillActions(): UseSkillActionsResult {
|
|
|
88
100
|
|
|
89
101
|
let tmpDir: string
|
|
90
102
|
if (source.type === "skillsgate") {
|
|
91
|
-
// Download from
|
|
103
|
+
// Download from private API
|
|
92
104
|
const token = await getToken()
|
|
93
105
|
tmpDir = await downloadSkill(source.username!, source.slug!, token)
|
|
94
|
-
} else if (source.type === "github") {
|
|
95
|
-
// Clone from GitHub
|
|
96
|
-
tmpDir = await cloneRepo(source)
|
|
97
106
|
} else {
|
|
98
107
|
// Local path -- use directly
|
|
99
108
|
tmpDir = source.localPath!
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
try {
|
|
103
|
-
// Discover skills in the
|
|
112
|
+
// Discover skills in the downloaded directory
|
|
104
113
|
const skills = await discoverSkills(tmpDir, source.subpath)
|
|
105
114
|
|
|
106
115
|
if (skills.length === 0) {
|
|
@@ -320,9 +329,49 @@ export function useSkillActions(): UseSkillActionsResult {
|
|
|
320
329
|
|
|
321
330
|
// ---------- Helpers ----------
|
|
322
331
|
|
|
332
|
+
const execAsync = promisify(execCb)
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Checks if a string matches the owner/repo format (e.g. "vercel/skills").
|
|
336
|
+
*/
|
|
337
|
+
function isOwnerRepoFormat(str: string): boolean {
|
|
338
|
+
return /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(str)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Runs `npx skills add <source> --all -y` as a child process to install
|
|
343
|
+
* all skills from a public repository.
|
|
344
|
+
*/
|
|
345
|
+
async function runSkillsAdd(
|
|
346
|
+
source: string,
|
|
347
|
+
dispatch: (action: Action) => void
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
try {
|
|
350
|
+
await execAsync(
|
|
351
|
+
`npx skills add ${source} --all -y`,
|
|
352
|
+
{ timeout: 60_000 }
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
dispatch({ type: "REFRESH_SKILLS" })
|
|
356
|
+
dispatch({
|
|
357
|
+
type: "SHOW_NOTIFICATION",
|
|
358
|
+
notification: {
|
|
359
|
+
type: "success",
|
|
360
|
+
message: `Installed skills from ${source}`,
|
|
361
|
+
},
|
|
362
|
+
})
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
365
|
+
dispatch({
|
|
366
|
+
type: "SHOW_NOTIFICATION",
|
|
367
|
+
notification: { type: "error", message: `Install failed: ${msg}` },
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
323
372
|
/**
|
|
324
373
|
* Resolves the source string for a skill from its metadata.
|
|
325
|
-
* Checks: lock.source, metadata.githubUrl, metadata.installCommand
|
|
374
|
+
* Checks: lock.source, metadata.source, metadata.githubUrl, metadata.installCommand
|
|
326
375
|
*/
|
|
327
376
|
function resolveSource(skill: EnrichedSkill): string | null {
|
|
328
377
|
// From lock file entry
|
|
@@ -330,17 +379,20 @@ function resolveSource(skill: EnrichedSkill): string | null {
|
|
|
330
379
|
return skill.lock.source
|
|
331
380
|
}
|
|
332
381
|
|
|
333
|
-
// From metadata (catalog skills)
|
|
382
|
+
// From metadata (catalog skills -- owner/repo format)
|
|
334
383
|
const meta = skill.metadata
|
|
384
|
+
if (meta?.source && typeof meta.source === "string") {
|
|
385
|
+
return meta.source
|
|
386
|
+
}
|
|
387
|
+
|
|
335
388
|
if (meta?.githubUrl && typeof meta.githubUrl === "string") {
|
|
336
389
|
return meta.githubUrl
|
|
337
390
|
}
|
|
338
391
|
|
|
339
|
-
// From install command (e.g. "
|
|
392
|
+
// From install command (e.g. "skills add <source>")
|
|
340
393
|
if (meta?.installCommand && typeof meta.installCommand === "string") {
|
|
341
394
|
const cmd = meta.installCommand as string
|
|
342
|
-
|
|
343
|
-
const match = cmd.match(/skillsgate\s+(?:add|install)\s+(.+)/)
|
|
395
|
+
const match = cmd.match(/skills?\s+(?:add|install)\s+(.+)/)
|
|
344
396
|
if (match) {
|
|
345
397
|
return match[1].trim()
|
|
346
398
|
}
|
package/src/views/discover.tsx
CHANGED
|
@@ -4,31 +4,22 @@ import { useStore, useDispatch } from "../store/context.js"
|
|
|
4
4
|
import { useSearch } from "../data/use-search.js"
|
|
5
5
|
import { useSkillActions } from "../data/use-skill-actions.js"
|
|
6
6
|
import { ConfirmDialog } from "../components/confirm-dialog.js"
|
|
7
|
-
import type { CatalogSkill
|
|
7
|
+
import type { CatalogSkill } from "../data/api-client.js"
|
|
8
8
|
import { colors } from "../utils/colors.js"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Discover view: two-column layout with search
|
|
12
|
-
* LEFT - Search input +
|
|
11
|
+
* Discover view: two-column layout with search results.
|
|
12
|
+
* LEFT - Search input + results list (40%)
|
|
13
13
|
* RIGHT - Selected result detail (flexGrow)
|
|
14
|
-
*
|
|
15
|
-
* Search modes:
|
|
16
|
-
* - Keyword (public, no auth): ILIKE pattern matching, unlimited
|
|
17
|
-
* - Semantic (auth required): AI-powered vector search, 30/day limit
|
|
18
|
-
* Toggle with 'm' key.
|
|
19
14
|
*/
|
|
20
15
|
export function DiscoverView() {
|
|
21
16
|
const state = useStore()
|
|
22
17
|
const dispatch = useDispatch()
|
|
23
18
|
const [query, setQuery] = useState("")
|
|
24
|
-
const [searchMode, setSearchMode] = useState<SearchMode>("keyword")
|
|
25
19
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
26
20
|
const [installTarget, setInstallTarget] = useState<CatalogSkill | null>(null)
|
|
27
21
|
const [previewSkill, setPreviewSkill] = useState<CatalogSkill | null>(null)
|
|
28
22
|
|
|
29
|
-
const token = state.auth?.token ?? null
|
|
30
|
-
const isAuthenticated = !!state.auth
|
|
31
|
-
|
|
32
23
|
// Auto-focus search input when Discover view mounts
|
|
33
24
|
useEffect(() => {
|
|
34
25
|
if (state.activeView === "discover") {
|
|
@@ -36,8 +27,8 @@ export function DiscoverView() {
|
|
|
36
27
|
}
|
|
37
28
|
}, [state.activeView])
|
|
38
29
|
|
|
39
|
-
const { results, loading, error, total, hasMore, loadMore
|
|
40
|
-
useSearch(query
|
|
30
|
+
const { results, loading, error, total, hasMore, loadMore } =
|
|
31
|
+
useSearch(query)
|
|
41
32
|
const { installSkill } = useSkillActions()
|
|
42
33
|
|
|
43
34
|
// Update preview when selection changes
|
|
@@ -94,24 +85,6 @@ export function DiscoverView() {
|
|
|
94
85
|
setInstallTarget(results[selectedIndex])
|
|
95
86
|
return
|
|
96
87
|
}
|
|
97
|
-
|
|
98
|
-
// m to toggle search mode
|
|
99
|
-
if (key.name === "m") {
|
|
100
|
-
if (searchMode === "keyword") {
|
|
101
|
-
if (!isAuthenticated) {
|
|
102
|
-
dispatch({
|
|
103
|
-
type: "SHOW_NOTIFICATION",
|
|
104
|
-
notification: { type: "info", message: "Sign in to use AI search (press l)" },
|
|
105
|
-
})
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
setSearchMode("semantic")
|
|
109
|
-
} else {
|
|
110
|
-
setSearchMode("keyword")
|
|
111
|
-
}
|
|
112
|
-
setSelectedIndex(0)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
88
|
})
|
|
116
89
|
|
|
117
90
|
// Confirm dialog for install
|
|
@@ -142,17 +115,11 @@ export function DiscoverView() {
|
|
|
142
115
|
paddingLeft: 1,
|
|
143
116
|
paddingRight: 1,
|
|
144
117
|
}}
|
|
145
|
-
title={state.focusedPane === "search"
|
|
146
|
-
? (searchMode === "semantic" ? "AI Search" : "Keyword Search")
|
|
147
|
-
: "/ to search"}
|
|
118
|
+
title={state.focusedPane === "search" ? "Search" : "/ to search"}
|
|
148
119
|
>
|
|
149
120
|
{state.focusedPane === "search" ? (
|
|
150
121
|
<input
|
|
151
|
-
placeholder=
|
|
152
|
-
searchMode === "semantic"
|
|
153
|
-
? 'AI search -- try "audit website performance" (Enter to search)'
|
|
154
|
-
: "Search by keyword... (Enter to search)"
|
|
155
|
-
}
|
|
122
|
+
placeholder="Search skills... (Enter to search)"
|
|
156
123
|
focused={state.activeView === "discover" && !state.showHelp}
|
|
157
124
|
onSubmit={((value: string) => {
|
|
158
125
|
setQuery(value)
|
|
@@ -164,7 +131,7 @@ export function DiscoverView() {
|
|
|
164
131
|
)}
|
|
165
132
|
</box>
|
|
166
133
|
|
|
167
|
-
{/* Status line
|
|
134
|
+
{/* Status line */}
|
|
168
135
|
<box
|
|
169
136
|
style={{
|
|
170
137
|
height: 1,
|
|
@@ -174,37 +141,16 @@ export function DiscoverView() {
|
|
|
174
141
|
flexDirection: "row",
|
|
175
142
|
}}
|
|
176
143
|
>
|
|
177
|
-
{/* Mode toggle: show both options, highlight active */}
|
|
178
|
-
<text fg={searchMode === "keyword" ? colors.primary : colors.textDim}>
|
|
179
|
-
{searchMode === "keyword" ? "[Keyword]" : " Keyword "}
|
|
180
|
-
</text>
|
|
181
|
-
<text fg={colors.textDim}>{" | "}</text>
|
|
182
|
-
<text fg={searchMode === "semantic" ? colors.warning : colors.textDim}>
|
|
183
|
-
{searchMode === "semantic" ? "[AI Search]" : (isAuthenticated ? " AI Search " : " AI Search (login) ")}
|
|
184
|
-
</text>
|
|
185
|
-
<text fg={colors.textDim}>{" m=switch "}</text>
|
|
186
|
-
|
|
187
144
|
{/* Results info */}
|
|
188
145
|
<text fg={colors.textDim}>
|
|
189
146
|
{loading
|
|
190
147
|
? "Loading..."
|
|
191
148
|
: error
|
|
192
|
-
? error
|
|
193
|
-
? "Session expired -- press l to re-login"
|
|
194
|
-
: error === "RATE_LIMIT"
|
|
195
|
-
? "Daily limit reached -- switch to keyword (m)"
|
|
196
|
-
: `Error: ${error}`
|
|
149
|
+
? `Error: ${error}`
|
|
197
150
|
: query.trim()
|
|
198
151
|
? `${results.length} result${results.length !== 1 ? "s" : ""}`
|
|
199
152
|
: `${results.length}/${total} skills`}
|
|
200
153
|
</text>
|
|
201
|
-
|
|
202
|
-
{/* Remaining searches (semantic only) */}
|
|
203
|
-
{searchMode === "semantic" && remainingSearches !== null ? (
|
|
204
|
-
<text fg={remainingSearches <= 5 ? colors.error : colors.textDim}>
|
|
205
|
-
{` ${remainingSearches} left today`}
|
|
206
|
-
</text>
|
|
207
|
-
) : null}
|
|
208
154
|
</box>
|
|
209
155
|
|
|
210
156
|
{/* Two-column content: results list | detail */}
|
|
@@ -250,7 +196,7 @@ export function DiscoverView() {
|
|
|
250
196
|
>
|
|
251
197
|
{results.map((skill, i) => (
|
|
252
198
|
<box
|
|
253
|
-
key={skill.id ?? `${skill.
|
|
199
|
+
key={skill.id ?? `${skill.skillId}-${i}`}
|
|
254
200
|
style={{
|
|
255
201
|
width: "100%",
|
|
256
202
|
paddingLeft: 1,
|
|
@@ -297,11 +243,6 @@ interface DiscoverDetailPanelProps {
|
|
|
297
243
|
}
|
|
298
244
|
|
|
299
245
|
function DiscoverDetailPanel({ skill }: DiscoverDetailPanelProps) {
|
|
300
|
-
const description = skill.summary || skill.description || ""
|
|
301
|
-
const categories = skill.categories?.join(", ") ?? ""
|
|
302
|
-
const capabilities = skill.capabilities?.join(", ") ?? ""
|
|
303
|
-
const keywords = skill.keywords?.join(", ") ?? ""
|
|
304
|
-
|
|
305
246
|
return (
|
|
306
247
|
<scrollbox
|
|
307
248
|
focused={false}
|
|
@@ -325,58 +266,25 @@ function DiscoverDetailPanel({ skill }: DiscoverDetailPanelProps) {
|
|
|
325
266
|
<strong>{skill.name}</strong>
|
|
326
267
|
</text>
|
|
327
268
|
|
|
328
|
-
{/*
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{categories ? (
|
|
334
|
-
<box style={{ flexDirection: "row", height: 1 }}>
|
|
335
|
-
<text fg={colors.textDim}>Categories: </text>
|
|
336
|
-
<text fg={colors.secondary}>{categories}</text>
|
|
337
|
-
</box>
|
|
338
|
-
) : null}
|
|
339
|
-
|
|
340
|
-
{/* Capabilities */}
|
|
341
|
-
{capabilities ? (
|
|
342
|
-
<box style={{ flexDirection: "row", height: 1 }}>
|
|
343
|
-
<text fg={colors.textDim}>Capabilities: </text>
|
|
344
|
-
<text fg={colors.secondary}>{capabilities}</text>
|
|
345
|
-
</box>
|
|
346
|
-
) : null}
|
|
347
|
-
|
|
348
|
-
{/* Keywords */}
|
|
349
|
-
{keywords ? (
|
|
350
|
-
<box style={{ flexDirection: "row", height: 1 }}>
|
|
351
|
-
<text fg={colors.textDim}>Keywords: </text>
|
|
352
|
-
<text fg={colors.secondary}>{keywords}</text>
|
|
353
|
-
</box>
|
|
354
|
-
) : null}
|
|
355
|
-
|
|
356
|
-
{/* Score (semantic search only) */}
|
|
357
|
-
{skill.score ? (
|
|
358
|
-
<box style={{ flexDirection: "row", height: 1 }}>
|
|
359
|
-
<text fg={colors.textDim}>Score: </text>
|
|
360
|
-
<text fg={colors.success}>{skill.score.toFixed(3)}</text>
|
|
361
|
-
</box>
|
|
362
|
-
) : null}
|
|
269
|
+
{/* Skill ID */}
|
|
270
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
271
|
+
<text fg={colors.textDim}>ID: </text>
|
|
272
|
+
<text fg={colors.text}>{skill.skillId}</text>
|
|
273
|
+
</box>
|
|
363
274
|
|
|
364
|
-
{/*
|
|
365
|
-
{skill.
|
|
275
|
+
{/* Source (owner/repo) */}
|
|
276
|
+
{skill.source ? (
|
|
366
277
|
<box style={{ flexDirection: "row", height: 1 }}>
|
|
367
|
-
<text fg={colors.textDim}>
|
|
368
|
-
<text fg={colors.primary}>{skill.
|
|
278
|
+
<text fg={colors.textDim}>Source: </text>
|
|
279
|
+
<text fg={colors.primary}>{skill.source}</text>
|
|
369
280
|
</box>
|
|
370
281
|
) : null}
|
|
371
282
|
|
|
372
|
-
{/*
|
|
373
|
-
{
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
<text fg={colors.success}> {skill.installCommand}</text>
|
|
378
|
-
</>
|
|
379
|
-
) : null}
|
|
283
|
+
{/* Installs */}
|
|
284
|
+
<box style={{ flexDirection: "row", height: 1 }}>
|
|
285
|
+
<text fg={colors.textDim}>Installs: </text>
|
|
286
|
+
<text fg={colors.success}>{skill.installs.toLocaleString()}</text>
|
|
287
|
+
</box>
|
|
380
288
|
|
|
381
289
|
<text>{" "}</text>
|
|
382
290
|
<text fg={colors.textDim}>v=full detail i=install Tab=switch pane</text>
|
|
@@ -392,9 +300,11 @@ function DiscoverDetailPanel({ skill }: DiscoverDetailPanelProps) {
|
|
|
392
300
|
* Since catalog skills don't have a local file, we provide a placeholder.
|
|
393
301
|
*/
|
|
394
302
|
function catalogSkillToEnriched(skill: CatalogSkill): import("../store/types.js").EnrichedSkill {
|
|
303
|
+
const githubUrl = skill.source ? `https://github.com/${skill.source}` : undefined
|
|
304
|
+
|
|
395
305
|
return {
|
|
396
306
|
name: skill.name,
|
|
397
|
-
description:
|
|
307
|
+
description: "",
|
|
398
308
|
filePath: "", // No local file for catalog items
|
|
399
309
|
canonicalPath: "",
|
|
400
310
|
agents: [],
|
|
@@ -403,18 +313,15 @@ function catalogSkillToEnriched(skill: CatalogSkill): import("../store/types.js"
|
|
|
403
313
|
hasSupportingFiles: false,
|
|
404
314
|
supportingFiles: [],
|
|
405
315
|
metadata: {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
githubUrl: skill.githubUrl,
|
|
410
|
-
installCommand: skill.installCommand,
|
|
411
|
-
urlPath: skill.urlPath,
|
|
316
|
+
source: skill.source,
|
|
317
|
+
skillId: skill.skillId,
|
|
318
|
+
installs: skill.installs,
|
|
412
319
|
},
|
|
413
|
-
lock: skill.
|
|
320
|
+
lock: skill.source
|
|
414
321
|
? {
|
|
415
|
-
source: skill.
|
|
322
|
+
source: skill.source,
|
|
416
323
|
sourceType: "github" as const,
|
|
417
|
-
originalUrl:
|
|
324
|
+
originalUrl: githubUrl ?? "",
|
|
418
325
|
skillFolderHash: "",
|
|
419
326
|
installedAt: "",
|
|
420
327
|
updatedAt: "",
|