skillgo 1.0.0 → 1.0.2
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/README.md +3 -2
- package/bin/skillgo.js +161 -33
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SkillGo CLI
|
|
2
2
|
|
|
3
|
-
SkillGo 命令行工具 - 搜索、安装、管理 Agent
|
|
3
|
+
SkillGo 命令行工具 - 搜索、安装、管理 Agent 技能。安装时会递归下载 SKILL.md 及同目录、子目录下的所有代码文件。支持中文技能名,若 slug 未匹配会自动按名称搜索。https://skillgo.cn
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -23,4 +23,5 @@ skillgo update --all # 更新所有已安装技能
|
|
|
23
23
|
|
|
24
24
|
## 环境变量
|
|
25
25
|
|
|
26
|
-
- `SKILLGO_API`: API 地址,默认 `http://
|
|
26
|
+
- `SKILLGO_API`: API 地址,默认 `http://skillgo.cn`
|
|
27
|
+
- `SKILLGO_DEBUG`: 设为 `1` 启用调试模式,或使用 `--debug` / `-d` 参数
|
package/bin/skillgo.js
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const isDebug = () => process.env.SKILLGO_DEBUG === '1' || process.env.SKILLGO_DEBUG === 'true'
|
|
5
|
+
const API_BASE = process.env.SKILLGO_API || 'http://skillgo.cn'
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
if (!res.ok) throw new Error(`请求失败: ${res.status}`)
|
|
9
|
-
return res.json()
|
|
7
|
+
const debug = (...args) => {
|
|
8
|
+
if (isDebug()) console.error('[skillgo]', ...args)
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
const wrapFetch = (fn) => async (...args) => {
|
|
12
|
+
try {
|
|
13
|
+
return await fn(...args)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (isDebug()) {
|
|
16
|
+
console.error('[skillgo] 请求失败:', e.message)
|
|
17
|
+
if (e.cause) console.error('[skillgo] cause:', e.cause)
|
|
18
|
+
if (e.stack) console.error('[skillgo] stack:', e.stack)
|
|
19
|
+
}
|
|
20
|
+
const msg = e.message || ''
|
|
21
|
+
const isNetworkErr = msg.includes('fetch failed') || e.cause?.code === 'ECONNREFUSED' || e.cause?.code === 'ENOTFOUND'
|
|
22
|
+
if (isNetworkErr) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`无法连接 SkillGo API (${API_BASE})。请检查网络或设置 SKILLGO_API`
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
throw e
|
|
28
|
+
}
|
|
16
29
|
}
|
|
17
30
|
|
|
31
|
+
const fetchJson = wrapFetch(async (path) => {
|
|
32
|
+
const url = `${API_BASE}${path}`
|
|
33
|
+
debug('GET', url)
|
|
34
|
+
const start = Date.now()
|
|
35
|
+
const res = await fetch(url)
|
|
36
|
+
debug('响应', res.status, `${Date.now() - start}ms`)
|
|
37
|
+
if (!res.ok) throw new Error(`请求失败: ${res.status} ${res.statusText}`)
|
|
38
|
+
return res.json()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const fetchText = wrapFetch(async (path) => {
|
|
42
|
+
const url = `${API_BASE}${path}`
|
|
43
|
+
debug('GET', url)
|
|
44
|
+
const start = Date.now()
|
|
45
|
+
const res = await fetch(url)
|
|
46
|
+
debug('响应', res.status, `${Date.now() - start}ms`)
|
|
47
|
+
if (!res.ok) throw new Error(`请求失败: ${res.status} ${res.statusText}`)
|
|
48
|
+
return res.text()
|
|
49
|
+
})
|
|
50
|
+
|
|
18
51
|
const search = async (keyword) => {
|
|
19
52
|
const data = await fetchJson(`/api/skills?status=published&q=${encodeURIComponent(keyword)}&pageSize=20`)
|
|
20
53
|
const items = data.items || []
|
|
@@ -29,43 +62,121 @@ const search = async (keyword) => {
|
|
|
29
62
|
})
|
|
30
63
|
}
|
|
31
64
|
|
|
65
|
+
const fetchRepoDir = async (project, relPath, localDir, branch, isGitee, fs, path) => {
|
|
66
|
+
const apiPath = relPath || (isGitee ? '.' : '')
|
|
67
|
+
let contents = []
|
|
68
|
+
if (isGitee) {
|
|
69
|
+
const apiUrl = `https://gitee.com/api/v5/repos/${project}/contents/${apiPath}`
|
|
70
|
+
debug('获取目录', apiUrl)
|
|
71
|
+
try {
|
|
72
|
+
contents = await fetch(apiUrl).then((r) => (r.ok ? r.json() : []))
|
|
73
|
+
} catch {
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
const apiUrl = `https://api.github.com/repos/${project}/contents/${apiPath}`
|
|
78
|
+
debug('获取目录', apiUrl)
|
|
79
|
+
try {
|
|
80
|
+
contents = await fetch(apiUrl).then((r) => (r.ok ? r.json() : []))
|
|
81
|
+
} catch {
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(contents)) contents = [contents]
|
|
86
|
+
let count = 0
|
|
87
|
+
const filePath = (p, name) => (p ? `${p}/${name}` : name)
|
|
88
|
+
for (const f of contents) {
|
|
89
|
+
const name = f.name
|
|
90
|
+
if (f.type === 'dir') {
|
|
91
|
+
const subRelPath = filePath(relPath, name)
|
|
92
|
+
const subLocalDir = path.join(localDir, name)
|
|
93
|
+
await fs.mkdir(subLocalDir, { recursive: true })
|
|
94
|
+
count += await fetchRepoDir(project, subRelPath, subLocalDir, branch, isGitee, fs, path)
|
|
95
|
+
} else if (f.type === 'file') {
|
|
96
|
+
if (name === 'SKILL.md') continue
|
|
97
|
+
const fullPath = filePath(relPath, name)
|
|
98
|
+
const rawUrl = isGitee
|
|
99
|
+
? `https://gitee.com/${project}/raw/${branch}/${fullPath}`
|
|
100
|
+
: `https://raw.githubusercontent.com/${project}/${branch}/${fullPath}`
|
|
101
|
+
try {
|
|
102
|
+
const content = await fetch(rawUrl).then((r) => (r.ok ? r.text() : null))
|
|
103
|
+
if (content !== null) {
|
|
104
|
+
await fs.writeFile(path.join(localDir, name), content, 'utf-8')
|
|
105
|
+
debug('已下载', fullPath)
|
|
106
|
+
count += 1
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return count
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fetchRepoFiles = async (repoUrl, sourcePath, dir, fs, path) => {
|
|
115
|
+
const m = repoUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\/|\.git)?$/i) || repoUrl.match(/gitee\.com\/([^/]+\/[^/]+?)(?:\/|\.git)?$/i)
|
|
116
|
+
if (!m) return 0
|
|
117
|
+
const project = m[1]
|
|
118
|
+
const dirPath = sourcePath.replace(/\/SKILL\.md$/i, '').replace(/^\/+/, '')
|
|
119
|
+
if (!dirPath) return 0
|
|
120
|
+
const isGitee = /gitee\.com/i.test(repoUrl)
|
|
121
|
+
let branch = isGitee ? 'master' : 'main'
|
|
122
|
+
if (!isGitee) {
|
|
123
|
+
try {
|
|
124
|
+
const repoInfo = await fetch(`https://api.github.com/repos/${project}`).then((r) => (r.ok ? r.json() : null))
|
|
125
|
+
if (repoInfo?.default_branch) branch = repoInfo.default_branch
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
return fetchRepoDir(project, dirPath, dir, branch, isGitee, fs, path)
|
|
129
|
+
}
|
|
130
|
+
|
|
32
131
|
const install = async (slug, version, cwd) => {
|
|
33
|
-
|
|
132
|
+
debug('install', { slug, version, cwd })
|
|
133
|
+
const fs = await import('fs/promises')
|
|
134
|
+
const path = await import('path')
|
|
135
|
+
let skill
|
|
34
136
|
if (version) {
|
|
35
|
-
data = await fetchJson(`/api/skills?status=published&slug=${encodeURIComponent(slug)}&version=${encodeURIComponent(version)}`)
|
|
137
|
+
let data = await fetchJson(`/api/skills?status=published&slug=${encodeURIComponent(slug)}&version=${encodeURIComponent(version)}`)
|
|
138
|
+
let items = data.items || []
|
|
139
|
+
if (items.length === 0) {
|
|
140
|
+
debug('slug 未匹配,尝试按名称搜索')
|
|
141
|
+
data = await fetchJson(`/api/skills?status=published&q=${encodeURIComponent(slug)}&pageSize=50`)
|
|
142
|
+
items = (data.items || []).filter((s) => s.version === version)
|
|
143
|
+
}
|
|
144
|
+
debug('查询结果:', items.length, '条')
|
|
145
|
+
skill = items[0]
|
|
146
|
+
if (!skill) {
|
|
147
|
+
debug('未找到匹配技能,slug=', slug, 'version=', version)
|
|
148
|
+
console.error(`未找到技能: ${slug}${version ? '@' + version : ''}`)
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
36
151
|
} else {
|
|
152
|
+
debug('从 MCP 获取技能列表')
|
|
37
153
|
const all = await fetchJson('/mcp/skills?latest=true')
|
|
38
154
|
const skills = all.skills || []
|
|
39
|
-
|
|
155
|
+
debug('MCP 返回', skills.length, '个技能')
|
|
156
|
+
skill = skills.find((s) => s.slug === slug || s.name === slug)
|
|
40
157
|
if (!skill) {
|
|
41
158
|
console.error(`未找到技能: ${slug}`)
|
|
42
159
|
process.exit(1)
|
|
43
160
|
}
|
|
44
|
-
const text = await fetchText(`/api/skills/${skill.id}/skill`)
|
|
45
|
-
const fs = await import('fs/promises')
|
|
46
|
-
const path = await import('path')
|
|
47
|
-
const dir = path.join(cwd, 'skills', skill.slug)
|
|
48
|
-
await fs.mkdir(dir, { recursive: true })
|
|
49
|
-
await fs.writeFile(path.join(dir, 'SKILL.md'), text, 'utf-8')
|
|
50
|
-
console.log(`已安装 ${skill.slug}@${skill.version} 到 ${dir}/SKILL.md`)
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
const items = data.items || []
|
|
54
|
-
const skill = items[0]
|
|
55
|
-
if (!skill) {
|
|
56
|
-
console.error(`未找到技能: ${slug}${ver ? '@' + ver : ''}`)
|
|
57
|
-
process.exit(1)
|
|
58
161
|
}
|
|
59
162
|
if (skill.review_tier === 'none' || !skill.review_tier) {
|
|
60
163
|
console.warn('警告:该技能未经审核,使用前请自行评估安全风险。')
|
|
61
164
|
}
|
|
62
165
|
const text = await fetchText(`/api/skills/${skill.id}/skill`)
|
|
63
|
-
const fs = await import('fs/promises')
|
|
64
|
-
const path = await import('path')
|
|
65
166
|
const dir = path.join(cwd, 'skills', skill.slug)
|
|
66
167
|
await fs.mkdir(dir, { recursive: true })
|
|
67
168
|
await fs.writeFile(path.join(dir, 'SKILL.md'), text, 'utf-8')
|
|
68
|
-
|
|
169
|
+
let extra = 0
|
|
170
|
+
if (skill.repo_url && skill.source_path) {
|
|
171
|
+
const detail = await fetchJson(`/api/skills/${skill.id}`)
|
|
172
|
+
const repoUrl = detail.repo_url || skill.repo_url
|
|
173
|
+
const sourcePath = detail.source_path || skill.source_path
|
|
174
|
+
if (repoUrl && sourcePath) {
|
|
175
|
+
extra = await fetchRepoFiles(repoUrl, sourcePath, dir, fs, path)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const files = extra > 0 ? ` (含 ${extra} 个代码文件)` : ''
|
|
179
|
+
console.log(`已安装 ${skill.slug}@${skill.version} 到 ${dir}${files}`)
|
|
69
180
|
}
|
|
70
181
|
|
|
71
182
|
const list = async (cwd) => {
|
|
@@ -119,7 +230,15 @@ const update = async (cwd) => {
|
|
|
119
230
|
}
|
|
120
231
|
|
|
121
232
|
const main = async () => {
|
|
122
|
-
|
|
233
|
+
let args = process.argv.slice(2)
|
|
234
|
+
if (args.includes('--debug') || args.includes('-d')) {
|
|
235
|
+
args = args.filter((a) => a !== '--debug' && a !== '-d')
|
|
236
|
+
process.env.SKILLGO_DEBUG = '1'
|
|
237
|
+
}
|
|
238
|
+
if (isDebug()) {
|
|
239
|
+
debug('API_BASE:', process.env.SKILLGO_API || '(默认)', '->', API_BASE)
|
|
240
|
+
debug('命令:', args[0], '参数:', args.slice(1))
|
|
241
|
+
}
|
|
123
242
|
const cmd = args[0]
|
|
124
243
|
const cwd = process.cwd()
|
|
125
244
|
|
|
@@ -162,16 +281,25 @@ SkillGo CLI - 技能管理工具 (https://skillgo.cn)
|
|
|
162
281
|
用法:
|
|
163
282
|
skillgo search <keyword> 搜索技能
|
|
164
283
|
skillgo install <slug> 安装技能到当前目录 skills/
|
|
165
|
-
skillgo install <slug>@<ver>
|
|
284
|
+
skillgo install <slug>@<ver> 安装指定版本(支持中文名/slug)
|
|
166
285
|
skillgo list 列出已安装技能
|
|
167
286
|
skillgo update --all 更新所有已安装技能
|
|
168
287
|
|
|
288
|
+
选项:
|
|
289
|
+
--debug, -d 调试模式,输出请求详情和错误堆栈
|
|
290
|
+
|
|
169
291
|
环境变量:
|
|
170
|
-
SKILLGO_API
|
|
292
|
+
SKILLGO_API API 地址,默认 http://skillgo.cn
|
|
293
|
+
SKILLGO_DEBUG 设为 1 启用调试模式
|
|
171
294
|
`)
|
|
172
295
|
}
|
|
173
296
|
|
|
174
297
|
main().catch((e) => {
|
|
175
|
-
|
|
298
|
+
if (isDebug()) {
|
|
299
|
+
console.error('[skillgo] 错误:', e.message)
|
|
300
|
+
if (e.stack) console.error(e.stack)
|
|
301
|
+
} else {
|
|
302
|
+
console.error(e.message)
|
|
303
|
+
}
|
|
176
304
|
process.exit(1)
|
|
177
305
|
})
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillgo",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "SkillGo 命令行工具 - 搜索、安装、管理 Agent 技能 (https://skillgo.cn)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skillgo": "bin/skillgo.js"
|
|
7
7
|
},
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"skillgo",
|
|
10
|
+
"cli",
|
|
11
|
+
"skill",
|
|
12
|
+
"agent",
|
|
13
|
+
"skillgo.cn"
|
|
14
|
+
],
|
|
9
15
|
"author": "",
|
|
10
16
|
"license": "Apache-2.0",
|
|
11
17
|
"repository": {
|