harveyz-skill 0.25.0 → 0.26.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.
- package/CHANGELOG.md +14 -0
- package/README.md +35 -1
- package/bin/cli.js +79 -28
- package/bin/preview.mjs +3 -3
- package/lib/mcp-server.js +173 -0
- package/lib/targets.js +3 -1
- package/package.json +8 -4
- package/skills/coding/handoff/SKILL.md +65 -0
- package/skills/coding/handoff/assets/handoff-template.md +45 -0
- package/skills/coding/handoff/evals/evals.json +33 -0
- package/skills/coding/handoff/evals/fixtures/accept/handoff-slugify.md +30 -0
- package/skills/coding/handoff/evals/fixtures/accept/slugify.js +18 -0
- package/skills/coding/handoff/references/config-schema.md +23 -0
- package/skills/research/clip-url/SKILL.md +142 -0
- package/skills/research/clip-url/references/subagent-self-optimize-prompt.md +130 -0
- package/skills/research/clip-url/references/subagent1-fetch-prompt.md +67 -0
- package/skills/research/clip-url/references/subagent2-tag-translate-prompt.md +122 -0
- package/skills/research/clip-url/scripts/__pycache__/article_meta.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/__pycache__/dedup_check.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/__pycache__/mcp_debug_client.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/__pycache__/mcp_fetch_client.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/__pycache__/vault_config.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/__pycache__/write_meta_and_separate.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/article_meta.py +105 -0
- package/skills/research/clip-url/scripts/chrome_profile_config.py +80 -0
- package/skills/research/clip-url/scripts/dedup_check.py +38 -0
- package/skills/research/clip-url/scripts/detect_xcom_chrome_profile.py +84 -0
- package/skills/research/clip-url/scripts/mcp_debug_client.py +63 -0
- package/skills/research/clip-url/scripts/mcp_fetch_client.py +107 -0
- package/skills/research/clip-url/scripts/vault_config.py +49 -0
- package/skills/research/clip-url/scripts/write_meta_and_separate.py +50 -0
- package/skills/research/clip-url/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_article_meta.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_chrome_profile_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_dedup_check.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_detect_xcom_chrome_profile.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_mcp_debug_client.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_mcp_fetch_client.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_vault_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_write_meta_and_separate.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/conftest.py +20 -0
- package/skills/research/clip-url/tests/test_article_meta.py +106 -0
- package/skills/research/clip-url/tests/test_chrome_profile_config.py +46 -0
- package/skills/research/clip-url/tests/test_dedup_check.py +63 -0
- package/skills/research/clip-url/tests/test_detect_xcom_chrome_profile.py +78 -0
- package/skills/research/clip-url/tests/test_mcp_debug_client.py +38 -0
- package/skills/research/clip-url/tests/test_mcp_fetch_client.py +140 -0
- package/skills/research/clip-url/tests/test_vault_config.py +44 -0
- package/skills/research/clip-url/tests/test_write_meta_and_separate.py +88 -0
- package/skills/research/extract-url/SKILL.md +2 -1
- package/skills/research/extract-url/platforms/SKILL.pi.md +41 -0
- package/skills/research/extract-url/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/research/extract-url/references/subagent1-fetch-prompt.md +9 -0
- package/skills/research/extract-url/scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/skills/research/extract-url/scripts/__pycache__/migrate_to_folder_structure.cpython-314.pyc +0 -0
- package/skills/research/extract-url/scripts/playwright_web_wechat.py +256 -0
- package/skills/research/extract-url/scripts/playwright_xcom.py +118 -0
- package/skills/research/extract-url/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_article_utils_meta.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_config.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_dedup_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_migrate_to_folder_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web_arxiv.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web_wechat.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_xcom.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_subagent1_prompt.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_subagent2_prompt.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_validate_article.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/test_playwright_web_wechat.py +191 -0
- package/skills/research/extract-url/tests/test_playwright_xcom.py +103 -1
- package/skills/research/pdf-math-translate/SKILL.md +1 -1
- package/skills-index.json +20 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.26.0] - 2026-08-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hskill mcp`:新增 MCP(Model Context Protocol)服务器子命令,以 stdio 方式暴露 8 个工具(`hskill_list`/`hskill_status`/`hskill_outdated`/`hskill_info`/`hskill_install`/`hskill_uninstall`/`hskill_hooks`/`hskill_update`),供 MCP 兼容的 agent host(Claude Code、Claude Desktop、Cursor 等)直接调用 hskill 的操作
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `publish-skill` 审计:修正 `pdf-math-translate` description 语言违规(改为英文),刷新 `capture-vocab`/`extract-url`/`pdf-math-translate` 三个 skill 的 contentHash 记录
|
|
17
|
+
|
|
18
|
+
## [0.25.1] - 2026-07-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `extract-url`:新增微信公众号文章抓取脚本(`playwright_web_wechat.py`),修复隐藏正文、懒加载图片、发布日期提取失败问题
|
|
22
|
+
- `extract-url`:修复 X.com Article 正文中加粗文字被错误拆分为独立段落、加粗样式丢失的问题
|
|
23
|
+
|
|
10
24
|
## [0.25.0] - 2026-07-20
|
|
11
25
|
|
|
12
26
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# harveyz-skill
|
|
2
2
|
|
|
3
|
-
Harvey 的个人 Claude Code / Cursor / Codex 技能管理器,当前版本 **v0.6.2**。
|
|
3
|
+
Harvey 的个人 Claude Code / Cursor / Codex / OpenClaw / Hermes / OpenCode / Pi 技能管理器,当前版本 **v0.6.2**。
|
|
4
4
|
|
|
5
5
|
通过 `hskill` 命令将技能安装到 `~/.claude/skills/`,按 bundle 分组管理。
|
|
6
6
|
|
|
@@ -134,6 +134,40 @@ hskill hooks uninstall check-similar-branch --scope project # 从当前项目
|
|
|
134
134
|
|
|
135
135
|
---
|
|
136
136
|
|
|
137
|
+
## MCP Server
|
|
138
|
+
|
|
139
|
+
`hskill mcp` 会启动一个基于 stdio 的 MCP(Model Context Protocol)服务器,让 MCP 兼容的 agent host(Claude Code、Claude Desktop、Cursor 等)可以直接调用 hskill 的操作,而不需要人工手动执行 CLI 命令。
|
|
140
|
+
|
|
141
|
+
暴露的 8 个工具:
|
|
142
|
+
|
|
143
|
+
| 工具 | 说明 |
|
|
144
|
+
|------|------|
|
|
145
|
+
| **hskill_list** | 列出所有可用的 skill 和 bundle |
|
|
146
|
+
| **hskill_status** | 显示所有 skill、工具、hook 在各个 target 上的安装状态 |
|
|
147
|
+
| **hskill_outdated** | 列出有可用更新的 skill 和工具 |
|
|
148
|
+
| **hskill_info** | 显示指定 skill 或工具的安装详情 |
|
|
149
|
+
| **hskill_install** | 安装一个 bundle、指定 skill 或 shell 工具 |
|
|
150
|
+
| **hskill_uninstall** | 卸载指定的 skill 或 shell 工具 |
|
|
151
|
+
| **hskill_hooks** | 查看、安装或卸载 hskill git hooks |
|
|
152
|
+
| **hskill_update** | 将 hskill 自身更新到最新版本 |
|
|
153
|
+
|
|
154
|
+
在 MCP host 的配置中注册(假设已通过 `npm install -g harveyz-skill` 全局安装):
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"mcpServers": {
|
|
159
|
+
"hskill": {
|
|
160
|
+
"command": "hskill",
|
|
161
|
+
"args": ["mcp"]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
本地源码开发时,可将 `command` 换成 `node`,`args` 换成 `["bin/cli.js", "mcp"]`。
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
137
171
|
## Skill 开发指南
|
|
138
172
|
|
|
139
173
|
### 目录结构
|
package/bin/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ const jsonFlag = args.includes('--json')
|
|
|
27
27
|
// ── Help ─────────────────────────────────────────────────────────────────────
|
|
28
28
|
function printHelp() {
|
|
29
29
|
console.log(`
|
|
30
|
-
${chalk.bold('hskill')} — skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, and
|
|
30
|
+
${chalk.bold('hskill')} — skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi v${version}
|
|
31
31
|
|
|
32
32
|
${chalk.cyan('Usage:')}
|
|
33
33
|
hskill interactive install (requires TTY + fzf)
|
|
@@ -35,7 +35,7 @@ function printHelp() {
|
|
|
35
35
|
hskill install --bundle <b> install a skill bundle
|
|
36
36
|
hskill install --skill <s> install specific skill(s)
|
|
37
37
|
hskill install --tool <t> install shell tool(s)
|
|
38
|
-
hskill install --target <t> set target (
|
|
38
|
+
hskill install --target <t> set target (${SKILL_TARGETS.join('/')}/all)
|
|
39
39
|
hskill install --scope <s> set scope: user (default) or project
|
|
40
40
|
hskill install --force overwrite existing installs
|
|
41
41
|
hskill list [--json] list available skills and bundles
|
|
@@ -51,6 +51,7 @@ function printHelp() {
|
|
|
51
51
|
hskill uninstall <tool> --yes skip all confirmations (incl. config files)
|
|
52
52
|
hskill uninstall <skill> --scope <s> --target <t> uninstall a skill
|
|
53
53
|
hskill update update hskill to the latest version
|
|
54
|
+
hskill mcp start an MCP server (stdio) exposing hskill's tools to MCP-capable agent hosts
|
|
54
55
|
hskill version show version
|
|
55
56
|
hskill --help show this help
|
|
56
57
|
|
|
@@ -75,7 +76,7 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
75
76
|
console.log(JSON.stringify({
|
|
76
77
|
name: 'hskill',
|
|
77
78
|
version,
|
|
78
|
-
description: 'Skill manager for Claude Code, Cursor, Codex, OpenClaw, and
|
|
79
|
+
description: 'Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi',
|
|
79
80
|
agent_notes: 'Interactive mode requires TTY. Use --json for machine-readable output. Set NO_COLOR=1 to suppress ANSI codes.',
|
|
80
81
|
commands: [
|
|
81
82
|
{
|
|
@@ -87,7 +88,7 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
87
88
|
{ name: '--bundle', arg: '<name>', description: 'Install a skill bundle (comma-separated)' },
|
|
88
89
|
{ name: '--skill', arg: '<name>', description: 'Install specific skill(s) (comma-separated)' },
|
|
89
90
|
{ name: '--tool', arg: '<name>', description: 'Install shell tool(s) (comma-separated)' },
|
|
90
|
-
{ name: '--target', arg: '<target>', description: 'Install target', enum: [
|
|
91
|
+
{ name: '--target', arg: '<target>', description: 'Install target', enum: [...SKILL_TARGETS,'all'] },
|
|
91
92
|
{ name: '--scope', arg: '<scope>', description: 'Install scope', enum: ['user','project'], default: 'user' },
|
|
92
93
|
{ name: '--force', description: 'Overwrite existing installs' },
|
|
93
94
|
],
|
|
@@ -129,7 +130,7 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
129
130
|
note: 'Only upgrades skills already installed on the given target. Never installs new ones.',
|
|
130
131
|
flags: [
|
|
131
132
|
{ name: '--skill', arg: '<name>', description: 'Upgrade a specific skill (default: all installed)' },
|
|
132
|
-
{ name: '--target', arg: '<target>', description: 'Limit to one target', enum:
|
|
133
|
+
{ name: '--target', arg: '<target>', description: 'Limit to one target', enum: SKILL_TARGETS },
|
|
133
134
|
{ name: '--scope', arg: '<scope>', description: 'Install scope', enum: ['user','project'], default: 'user' },
|
|
134
135
|
{ name: '--json', description: 'Machine-readable output' },
|
|
135
136
|
],
|
|
@@ -138,6 +139,10 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
138
139
|
name: 'update',
|
|
139
140
|
description: 'Update hskill to the latest version via npm',
|
|
140
141
|
},
|
|
142
|
+
{
|
|
143
|
+
name: 'mcp',
|
|
144
|
+
description: "Start an MCP server (stdio) exposing hskill's tools to MCP-capable agent hosts",
|
|
145
|
+
},
|
|
141
146
|
{
|
|
142
147
|
name: 'version',
|
|
143
148
|
description: 'Print version and exit',
|
|
@@ -189,6 +194,20 @@ if (subcommand === 'update') {
|
|
|
189
194
|
process.exit(0)
|
|
190
195
|
}
|
|
191
196
|
|
|
197
|
+
// ── MCP server ───────────────────────────────────────────────────────────────
|
|
198
|
+
if (subcommand === 'mcp') {
|
|
199
|
+
const { startServer } = await import('../lib/mcp-server.js')
|
|
200
|
+
await startServer()
|
|
201
|
+
// StdioServerTransport keeps stdin/stdout open for JSON-RPC. bin/cli.js is a
|
|
202
|
+
// flat top-level script with no early-return mechanism (this is a real ES
|
|
203
|
+
// module, so a bare top-level `return` is a syntax error) — every other
|
|
204
|
+
// subcommand block ends itself with process.exit(), which we can't do here
|
|
205
|
+
// without killing the server we just started. Blocking on a promise that
|
|
206
|
+
// never resolves is what stops execution from falling through into the
|
|
207
|
+
// generic install-arg parsing near the end of this file.
|
|
208
|
+
await new Promise(() => {})
|
|
209
|
+
}
|
|
210
|
+
|
|
192
211
|
// ── List ─────────────────────────────────────────────────────────────────────
|
|
193
212
|
if (subcommand === 'list') {
|
|
194
213
|
const { skills, tools = [] } = require('../skills-index.json')
|
|
@@ -285,7 +304,7 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
285
304
|
})
|
|
286
305
|
|
|
287
306
|
if (jsonFlag) {
|
|
288
|
-
const targets =
|
|
307
|
+
const targets = SKILL_TARGETS
|
|
289
308
|
const jsonSkills = skillRows.map(r => ({
|
|
290
309
|
name: r.name,
|
|
291
310
|
version: r.version,
|
|
@@ -317,7 +336,7 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
317
336
|
process.exit(0)
|
|
318
337
|
}
|
|
319
338
|
|
|
320
|
-
const targets =
|
|
339
|
+
const targets = SKILL_TARGETS
|
|
321
340
|
|
|
322
341
|
if (outdatedSkills.length) {
|
|
323
342
|
console.log('\n ' + chalk.bold('SKILLS WITH UPDATES'))
|
|
@@ -420,7 +439,7 @@ if (subcommand === 'info') {
|
|
|
420
439
|
process.exit(1)
|
|
421
440
|
}
|
|
422
441
|
|
|
423
|
-
const targets =
|
|
442
|
+
const targets = SKILL_TARGETS
|
|
424
443
|
|
|
425
444
|
function statusLabel(status) {
|
|
426
445
|
if (status === 'up-to-date') return chalk.green('✓ up to date')
|
|
@@ -476,7 +495,9 @@ if (subcommand === 'info') {
|
|
|
476
495
|
if (subcommand === 'uninstall') {
|
|
477
496
|
const nameToRemove = args[1]
|
|
478
497
|
if (!nameToRemove || nameToRemove.startsWith('--')) {
|
|
479
|
-
|
|
498
|
+
const msg = 'Usage: hskill uninstall <tool-or-skill-name> [--yes] [--scope user|project] [--target claude|...]'
|
|
499
|
+
if (jsonFlag) process.stderr.write(JSON.stringify({ error: true, message: msg }) + '\n')
|
|
500
|
+
else console.error(chalk.red(' ✗ ' + msg))
|
|
480
501
|
process.exit(1)
|
|
481
502
|
}
|
|
482
503
|
|
|
@@ -492,13 +513,24 @@ if (subcommand === 'uninstall') {
|
|
|
492
513
|
const isSkill = skillItems2.some(s => s.skillName === nameToRemove)
|
|
493
514
|
|
|
494
515
|
if (!isTool && !isSkill) {
|
|
495
|
-
|
|
516
|
+
const msg = `Unknown tool or skill: "${nameToRemove}"`
|
|
517
|
+
if (jsonFlag) process.stderr.write(JSON.stringify({ error: true, message: msg }) + '\n')
|
|
518
|
+
else console.error(chalk.red(' ✗ ' + msg))
|
|
496
519
|
process.exit(1)
|
|
497
520
|
}
|
|
498
521
|
|
|
499
522
|
if (isTool) {
|
|
523
|
+
// uninstallTool logs unconditional chalk status lines via console.error;
|
|
524
|
+
// suppress them in --json mode so they don't pollute stderr JSON output.
|
|
525
|
+
const originalError = jsonFlag ? console.error : null
|
|
526
|
+
if (jsonFlag) console.error = () => {}
|
|
500
527
|
const { removed, failed } = await uninstallTool(nameToRemove, { yes: yesFlag })
|
|
501
|
-
if (
|
|
528
|
+
if (jsonFlag) {
|
|
529
|
+
console.error = originalError
|
|
530
|
+
console.log(JSON.stringify({ removed: removed.length > 0, failed: failed.length > 0 }, null, 2))
|
|
531
|
+
} else if (removed.length > 0) {
|
|
532
|
+
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
533
|
+
}
|
|
502
534
|
process.exit(failed.length ? 1 : 0)
|
|
503
535
|
}
|
|
504
536
|
|
|
@@ -506,17 +538,26 @@ if (subcommand === 'uninstall') {
|
|
|
506
538
|
const scope = scopeArg2
|
|
507
539
|
const selectedTargets = targetArg2
|
|
508
540
|
? [targetArg2]
|
|
509
|
-
:
|
|
541
|
+
: SKILL_TARGETS
|
|
510
542
|
const targets = resolveTargets(selectedTargets, scope)
|
|
511
543
|
|
|
512
544
|
let anyRemoved = false
|
|
513
545
|
let anyFailed = false
|
|
546
|
+
// uninstallSkill logs unconditional chalk status lines via console.error;
|
|
547
|
+
// suppress them in --json mode so they don't pollute stderr JSON output.
|
|
548
|
+
const originalError2 = jsonFlag ? console.error : null
|
|
549
|
+
if (jsonFlag) console.error = () => {}
|
|
514
550
|
for (const { dir } of targets) {
|
|
515
551
|
const { removed, failed } = await uninstallSkill(nameToRemove, dir)
|
|
516
552
|
if (removed.length) anyRemoved = true
|
|
517
553
|
if (failed.length) anyFailed = true
|
|
518
554
|
}
|
|
519
|
-
if (
|
|
555
|
+
if (jsonFlag) {
|
|
556
|
+
console.error = originalError2
|
|
557
|
+
console.log(JSON.stringify({ removed: anyRemoved, failed: anyFailed }, null, 2))
|
|
558
|
+
} else if (anyRemoved) {
|
|
559
|
+
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
560
|
+
}
|
|
520
561
|
process.exit(anyFailed ? 1 : 0)
|
|
521
562
|
}
|
|
522
563
|
|
|
@@ -635,11 +676,17 @@ if (subcommand === 'hooks') {
|
|
|
635
676
|
if (hooksSubcmd === 'uninstall') {
|
|
636
677
|
const nameToRemove = args[2]
|
|
637
678
|
if (!nameToRemove || nameToRemove.startsWith('--')) {
|
|
638
|
-
|
|
679
|
+
const msg = 'Usage: hskill hooks uninstall <name> [--scope user|project]'
|
|
680
|
+
if (hookJsonFlag) process.stderr.write(JSON.stringify({ error: true, message: msg }) + '\n')
|
|
681
|
+
else console.error(chalk.red(' ✗ ' + msg))
|
|
639
682
|
process.exit(1)
|
|
640
683
|
}
|
|
641
684
|
const { removed } = await uninstallHook(nameToRemove, hookScopeArg, hookProjectArg)
|
|
642
|
-
if (
|
|
685
|
+
if (hookJsonFlag) {
|
|
686
|
+
console.log(JSON.stringify({ removed }, null, 2))
|
|
687
|
+
} else if (!removed) {
|
|
688
|
+
console.log(chalk.dim(` · ${nameToRemove} was not installed in ${hookScopeArg} scope`))
|
|
689
|
+
}
|
|
643
690
|
process.exit(0)
|
|
644
691
|
}
|
|
645
692
|
|
|
@@ -721,20 +768,24 @@ const scopeArg = scopeIdx !== -1 ? installArgs[scopeIdx + 1] : undefined
|
|
|
721
768
|
const TOOL_BUNDLE_VALUES = new Set(TOOL_BUNDLE_CHOICES.map(c => c.value))
|
|
722
769
|
|
|
723
770
|
// ── Two-step target→scope selector ───────────────────────────────────────────
|
|
724
|
-
|
|
771
|
+
// Paths are derived from SKILL_TARGETS/userSkillDir (lib/targets.js) — the single
|
|
772
|
+
// source of truth — so adding a target there is enough to update every view here.
|
|
773
|
+
const TARGET_DIRS = SKILL_TARGETS.map(name => {
|
|
774
|
+
const real = userSkillDir(name)
|
|
775
|
+
return { name, real, disp: real.replace(os.homedir(), '~') }
|
|
776
|
+
})
|
|
725
777
|
|
|
726
778
|
function selectTargetThenScope() {
|
|
727
779
|
// Step 1: platform (target)
|
|
728
780
|
const targetInput = [
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
'codex ~/.codex/skills/',
|
|
732
|
-
'openclaw ~/.openclaw/skills/',
|
|
733
|
-
'hermes ~/.hermes/skills/',
|
|
734
|
-
'opencode ~/.config/opencode/skills/',
|
|
735
|
-
'all all 6 targets',
|
|
781
|
+
...TARGET_DIRS.map(({ name, disp }) => `${name.padEnd(8)} ${disp}/`),
|
|
782
|
+
`all all ${TARGET_DIRS.length} targets`,
|
|
736
783
|
].join('\n')
|
|
737
784
|
|
|
785
|
+
const allEchoLines = TARGET_DIRS.map(({ disp }) => `echo " ${disp}/";`).join(' ')
|
|
786
|
+
const caseArms = TARGET_DIRS.map(({ name, disp, real }) => `${name}) disp="${disp}/"; real="${real}/";;`).join(' ')
|
|
787
|
+
const previewScript = `t=$(echo {} | awk '{print $1}'); if [ "$t" = "all" ]; then echo ""; echo " 安装到所有平台:"; echo ""; ${allEchoLines} else case "$t" in ${caseArms} esac; echo ""; echo " 安装路径:"; echo ""; printf " \\033[36m%s\\033[0m\\n" "$disp"; echo ""; if [ -d "$real" ]; then echo " ✓ 目录已存在"; else echo " · 将自动新建"; fi; fi`
|
|
788
|
+
|
|
738
789
|
const targetResult = spawnSync('fzf', [
|
|
739
790
|
'--multi',
|
|
740
791
|
'--prompt= › ',
|
|
@@ -742,7 +793,7 @@ function selectTargetThenScope() {
|
|
|
742
793
|
'--layout=reverse',
|
|
743
794
|
'--border=rounded',
|
|
744
795
|
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
745
|
-
|
|
796
|
+
`--preview=${previewScript}`,
|
|
746
797
|
'--preview-window=right:45%:wrap',
|
|
747
798
|
], {
|
|
748
799
|
input: targetInput,
|
|
@@ -754,7 +805,7 @@ function selectTargetThenScope() {
|
|
|
754
805
|
|
|
755
806
|
const rawTargets = targetResult.stdout.trim().split('\n')
|
|
756
807
|
.map(l => l.trim().split(/\s+/)[0])
|
|
757
|
-
const expandedTargets = rawTargets.includes('all') ?
|
|
808
|
+
const expandedTargets = rawTargets.includes('all') ? SKILL_TARGETS : rawTargets
|
|
758
809
|
|
|
759
810
|
// Step 2: scope (user/project) — skip if all selected targets are user-only
|
|
760
811
|
const allUserOnly = expandedTargets.every(t => USER_ONLY_TARGETS.has(t))
|
|
@@ -1258,7 +1309,7 @@ try {
|
|
|
1258
1309
|
'--header= 从哪里卸载 · tab 多选 · enter 确认 · esc 取消',
|
|
1259
1310
|
'--layout=reverse', '--border=rounded',
|
|
1260
1311
|
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
1261
|
-
|
|
1312
|
+
`--preview=t=$(echo {} | awk '{print $1}'); if [ "$t" = "all" ]; then echo ""; echo " 从所有平台卸载:"; echo ""; ${TARGET_DIRS.map(({ disp }) => `echo " ${disp}/";`).join(' ')} else case "$t" in ${TARGET_DIRS.map(({ name, disp }) => `${name}) echo ""; echo " ${disp}/";;`).join(' ')} esac; fi`,
|
|
1262
1313
|
'--preview-window=right:45%:wrap',
|
|
1263
1314
|
], {
|
|
1264
1315
|
input: targetChoices2.map(c => c.name).join('\n') + '\nall — all targets',
|
|
@@ -1349,14 +1400,14 @@ try {
|
|
|
1349
1400
|
// When only --skill is given and no --target, use the combined selector.
|
|
1350
1401
|
if (targetArg) {
|
|
1351
1402
|
const scope = scopeArg ?? 'user'
|
|
1352
|
-
const selectedTargets = targetArg === 'all' ?
|
|
1403
|
+
const selectedTargets = targetArg === 'all' ? SKILL_TARGETS : [targetArg]
|
|
1353
1404
|
const targets = resolveTargets(selectedTargets, scope)
|
|
1354
1405
|
console.log('')
|
|
1355
1406
|
skillSummary = await installSkills(skillItems, targets, forceFlag)
|
|
1356
1407
|
console.log('')
|
|
1357
1408
|
} else {
|
|
1358
1409
|
if (!process.stdout.isTTY) {
|
|
1359
|
-
console.error(chalk.red(
|
|
1410
|
+
console.error(chalk.red(` ✗ Interactive target selection requires a TTY. Use --target ${SKILL_TARGETS.join('|')}|all.`))
|
|
1360
1411
|
process.exit(1)
|
|
1361
1412
|
}
|
|
1362
1413
|
const selectedST = selectTargetThenScope()
|
package/bin/preview.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs'
|
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
-
import { SKILL_TARGETS, USER_ONLY_TARGETS } from '../lib/targets.js'
|
|
6
|
+
import { SKILL_TARGETS, USER_ONLY_TARGETS, userSkillDir } from '../lib/targets.js'
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
|
|
@@ -89,7 +89,7 @@ function checkScope(targets, dirFn) {
|
|
|
89
89
|
// 单平台视图:只展示 ctrl-t 当前所在平台的状态,不展示全平台矩阵
|
|
90
90
|
if (platformKey && SKILL_TARGETS.includes(platformKey)) {
|
|
91
91
|
const cwd = process.cwd()
|
|
92
|
-
const userDir =
|
|
92
|
+
const userDir = userSkillDir(platformKey)
|
|
93
93
|
const projectDir = path.join(cwd, `.${platformKey}`, 'skills')
|
|
94
94
|
const [userDetail] = checkScope([platformKey], () => userDir)
|
|
95
95
|
const projectDetail = USER_ONLY_TARGETS.has(platformKey) || cwd === home
|
|
@@ -119,7 +119,7 @@ const userTargets = SKILL_TARGETS
|
|
|
119
119
|
const projectTargets = SKILL_TARGETS.filter(t => !USER_ONLY_TARGETS.has(t))
|
|
120
120
|
|
|
121
121
|
const cwd = process.cwd()
|
|
122
|
-
const userDetails = checkScope(userTargets, t =>
|
|
122
|
+
const userDetails = checkScope(userTargets, t => userSkillDir(t))
|
|
123
123
|
const projectDetails = cwd === home
|
|
124
124
|
? projectTargets.map(t => ({ tool: t, version: '—', status: 'none' }))
|
|
125
125
|
: checkScope(projectTargets, t => path.join(cwd, `.${t}`, 'skills'))
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { spawn } from 'child_process'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js')
|
|
10
|
+
|
|
11
|
+
// Spawns `node bin/cli.js <argv>` and collects its full stdout/stderr/exit code.
|
|
12
|
+
// Never rejects — callers branch on `code`.
|
|
13
|
+
export function runCli(argv) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const child = spawn(process.execPath, [CLI_PATH, ...argv], { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
16
|
+
child.stdout.setEncoding('utf8')
|
|
17
|
+
child.stderr.setEncoding('utf8')
|
|
18
|
+
let stdout = ''
|
|
19
|
+
let stderr = ''
|
|
20
|
+
child.stdout.on('data', (d) => { stdout += d })
|
|
21
|
+
child.stderr.on('data', (d) => { stderr += d })
|
|
22
|
+
child.on('close', (code) => resolve({ code, stdout, stderr }))
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Turns a runCli() result into an MCP CallToolResult. On success, the CLI's
|
|
27
|
+
// stdout JSON is forwarded as-is. On failure, stderr is parsed as the CLI's
|
|
28
|
+
// existing {error:true,message} shape; if that parse fails, the raw stderr
|
|
29
|
+
// text is used so a handler never throws on an unexpected crash.
|
|
30
|
+
export function toToolResult({ code, stdout, stderr }) {
|
|
31
|
+
if (code === 0) {
|
|
32
|
+
return { content: [{ type: 'text', text: stdout.trim() || '{}' }] }
|
|
33
|
+
}
|
|
34
|
+
// Some CLI commands write valid structured JSON to stdout even on a
|
|
35
|
+
// non-zero exit (e.g. uninstall's "found but removal failed" case).
|
|
36
|
+
// Forward that real payload instead of falling back to a generic message.
|
|
37
|
+
const trimmedStdout = stdout.trim()
|
|
38
|
+
if (trimmedStdout) {
|
|
39
|
+
try {
|
|
40
|
+
JSON.parse(trimmedStdout)
|
|
41
|
+
return { content: [{ type: 'text', text: trimmedStdout }], isError: true }
|
|
42
|
+
} catch {
|
|
43
|
+
// stdout wasn't JSON — fall through to the stderr-based message below
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let message = stderr.trim() || `hskill exited with code ${code}`
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(stderr.trim())
|
|
49
|
+
if (parsed && parsed.error) message = parsed.message
|
|
50
|
+
} catch {
|
|
51
|
+
const lastLine = stderr.trim().split('\n').pop()
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(lastLine)
|
|
54
|
+
if (parsed && parsed.error) message = parsed.message
|
|
55
|
+
} catch {
|
|
56
|
+
// stderr wasn't JSON — fall back to the raw text already assigned above
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { content: [{ type: 'text', text: message }], isError: true }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createServer() {
|
|
63
|
+
const server = new McpServer({ name: 'hskill', version: '1.0.0' })
|
|
64
|
+
|
|
65
|
+
server.registerTool('hskill_list', {
|
|
66
|
+
description: 'List all available hskill skills and bundles.',
|
|
67
|
+
inputSchema: {},
|
|
68
|
+
}, async () => toToolResult(await runCli(['list', '--json'])))
|
|
69
|
+
|
|
70
|
+
server.registerTool('hskill_status', {
|
|
71
|
+
description: 'Show install status for all skills, tools, and hooks across every target.',
|
|
72
|
+
inputSchema: {},
|
|
73
|
+
}, async () => toToolResult(await runCli(['status', '--json'])))
|
|
74
|
+
|
|
75
|
+
server.registerTool('hskill_outdated', {
|
|
76
|
+
description: 'List only the skills and tools that have an available update.',
|
|
77
|
+
inputSchema: {},
|
|
78
|
+
}, async () => toToolResult(await runCli(['outdated', '--json'])))
|
|
79
|
+
|
|
80
|
+
server.registerTool('hskill_info', {
|
|
81
|
+
description: 'Show install detail for one named skill or tool, as returned by hskill_list.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
name: z.string().describe('Skill or tool name, exactly as shown by hskill_list'),
|
|
84
|
+
},
|
|
85
|
+
}, async ({ name }) => toToolResult(await runCli(['info', name, '--json'])))
|
|
86
|
+
|
|
87
|
+
server.registerTool('hskill_install', {
|
|
88
|
+
description: 'Install a skill bundle, a specific skill, or a shell tool. Writes files under ~/.claude/skills (or the equivalent dir for --target) or a project directory. Provide exactly one of bundle, skill, or tool.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
bundle: z.string().optional().describe('Bundle name(s) to install, comma-separated'),
|
|
91
|
+
skill: z.string().optional().describe('Specific skill name(s) to install, comma-separated'),
|
|
92
|
+
tool: z.string().optional().describe('Specific shell tool name(s) to install, comma-separated'),
|
|
93
|
+
target: z.string().optional().describe('Target host: claude, cursor, codex, opencode, etc., or "all"'),
|
|
94
|
+
scope: z.enum(['user', 'project']).optional().describe('Install scope, defaults to user'),
|
|
95
|
+
force: z.boolean().optional().describe('Overwrite an existing install'),
|
|
96
|
+
},
|
|
97
|
+
}, async ({ bundle, skill, tool, target, scope, force }) => {
|
|
98
|
+
const argv = ['install']
|
|
99
|
+
if (bundle) argv.push('--bundle', bundle)
|
|
100
|
+
if (skill) argv.push('--skill', skill)
|
|
101
|
+
if (tool) argv.push('--tool', tool)
|
|
102
|
+
if (target) argv.push('--target', target)
|
|
103
|
+
if (scope) argv.push('--scope', scope)
|
|
104
|
+
if (force) argv.push('--force')
|
|
105
|
+
argv.push('--json')
|
|
106
|
+
return toToolResult(await runCli(argv))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
server.registerTool('hskill_uninstall', {
|
|
110
|
+
description: 'Uninstall a named skill or shell tool, removing its files from disk. If target is omitted, the skill is removed from every install target (claude, cursor, codex, opencode, etc.), not just one.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
name: z.string().describe('Skill or tool name to uninstall, as shown by hskill_list'),
|
|
113
|
+
scope: z.enum(['user', 'project']).optional().describe('Scope to uninstall from, defaults to user'),
|
|
114
|
+
target: z.string().optional().describe('Target host to uninstall from'),
|
|
115
|
+
yes: z.boolean().optional().describe('Skip confirmation prompts'),
|
|
116
|
+
},
|
|
117
|
+
}, async ({ name, scope, target, yes }) => {
|
|
118
|
+
const argv = ['uninstall', name]
|
|
119
|
+
if (scope) argv.push('--scope', scope)
|
|
120
|
+
if (target) argv.push('--target', target)
|
|
121
|
+
if (yes) argv.push('--yes')
|
|
122
|
+
argv.push('--json')
|
|
123
|
+
return toToolResult(await runCli(argv))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
server.registerTool('hskill_hooks', {
|
|
127
|
+
description: 'List, install, or uninstall hskill git hooks. Install/uninstall write files under ~/.claude/hooks (or the project dir for --scope project) and register them in settings.json.',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
action: z.enum(['list', 'install', 'uninstall']),
|
|
130
|
+
name: z.string().optional().describe('Hook name — required for install/uninstall'),
|
|
131
|
+
scope: z.enum(['user', 'project']).optional().describe('Scope, defaults to user'),
|
|
132
|
+
project: z.string().optional().describe('Project directory, used when scope is project'),
|
|
133
|
+
force: z.boolean().optional().describe('Overwrite an existing hook install'),
|
|
134
|
+
},
|
|
135
|
+
}, async ({ action, name, scope, project, force }) => {
|
|
136
|
+
if (action !== 'list' && !name) {
|
|
137
|
+
return { content: [{ type: 'text', text: 'name is required for install/uninstall actions' }], isError: true }
|
|
138
|
+
}
|
|
139
|
+
const argv = ['hooks', action]
|
|
140
|
+
if (action === 'uninstall' && name) {
|
|
141
|
+
// uninstall expects hook name as positional argument
|
|
142
|
+
argv.push(name)
|
|
143
|
+
} else if (name) {
|
|
144
|
+
// install expects --name flag
|
|
145
|
+
argv.push('--name', name)
|
|
146
|
+
}
|
|
147
|
+
if (scope) argv.push('--scope', scope)
|
|
148
|
+
if (project) argv.push('--project', project)
|
|
149
|
+
if (force) argv.push('--force')
|
|
150
|
+
argv.push('--json')
|
|
151
|
+
return toToolResult(await runCli(argv))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
server.registerTool('hskill_update', {
|
|
155
|
+
description: 'Update hskill itself to the latest version via `npm install -g harveyz-skill@latest`. This replaces the globally installed hskill binary and is irreversible except by manually reinstalling a specific version.',
|
|
156
|
+
inputSchema: {},
|
|
157
|
+
}, async () => {
|
|
158
|
+
const { code, stdout, stderr } = await runCli(['update'])
|
|
159
|
+
if (code === 0) {
|
|
160
|
+
return { content: [{ type: 'text', text: 'hskill updated to the latest version.' }] }
|
|
161
|
+
}
|
|
162
|
+
const message = stderr.trim() || stdout.trim() || `update failed with exit code ${code}`
|
|
163
|
+
return { content: [{ type: 'text', text: message }], isError: true }
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return server
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function startServer() {
|
|
170
|
+
const server = createServer()
|
|
171
|
+
const transport = new StdioServerTransport()
|
|
172
|
+
await server.connect(transport)
|
|
173
|
+
}
|
package/lib/targets.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import os from 'os'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
|
|
4
|
-
export const SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes', 'opencode']
|
|
4
|
+
export const SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes', 'opencode', 'pi']
|
|
5
5
|
|
|
6
6
|
export const USER_ONLY_TARGETS = new Set(['openclaw', 'hermes'])
|
|
7
7
|
|
|
8
8
|
// Targets whose user-level skill dir doesn't follow ~/.{name}/skills
|
|
9
9
|
const USER_DIR_OVERRIDES = {
|
|
10
10
|
opencode: path.join(os.homedir(), '.config', 'opencode', 'skills'),
|
|
11
|
+
pi: path.join(os.homedir(), '.pi', 'agent', 'skills'),
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function userSkillDir(name) {
|
|
@@ -27,6 +28,7 @@ export const TARGETS = {
|
|
|
27
28
|
openclaw: path.join(os.homedir(), '.openclaw', 'skills'),
|
|
28
29
|
hermes: path.join(os.homedir(), '.hermes', 'skills'),
|
|
29
30
|
opencode: path.join(os.homedir(), '.config', 'opencode', 'skills'),
|
|
31
|
+
pi: path.join(os.homedir(), '.pi', 'agent', 'skills'),
|
|
30
32
|
shell: path.join(os.homedir(), '.local', 'bin'),
|
|
31
33
|
}
|
|
32
34
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Skill manager for Claude Code, Cursor, and
|
|
3
|
+
"version": "0.26.0",
|
|
4
|
+
"description": "Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hskill": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"prepack": "node scripts/generate-npmignore.js",
|
|
11
|
-
"test": "bats tests/ && bash scripts/run-skill-tests.sh"
|
|
11
|
+
"test": "bats tests/ && bash scripts/run-skill-tests.sh && node --test tests/mcp.test.mjs"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"skills/research/extract-cognition/",
|
|
26
26
|
"skills/research/probe-session/",
|
|
27
27
|
"skills/research/pdf-math-translate/",
|
|
28
|
+
"skills/research/clip-url/",
|
|
28
29
|
"skills/creative/capture-todo/",
|
|
29
30
|
"skills/creative/capture-insight/",
|
|
30
31
|
"skills/coding/init-workflow/",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"skills/meta/release-project/",
|
|
54
55
|
"skills/meta/sync-hotfix/",
|
|
55
56
|
"skills/meta/sync-agent/",
|
|
57
|
+
"skills/coding/handoff/",
|
|
56
58
|
"tools/hub/",
|
|
57
59
|
"tools/sync-agent/",
|
|
58
60
|
"hooks/check-similar-branch/"
|
|
@@ -62,8 +64,10 @@
|
|
|
62
64
|
},
|
|
63
65
|
"dependencies": {
|
|
64
66
|
"@inquirer/prompts": "^8.4.2",
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
65
68
|
"chalk": "^5.3.0",
|
|
66
|
-
"fs-extra": "^11.2.0"
|
|
69
|
+
"fs-extra": "^11.2.0",
|
|
70
|
+
"zod": "^4.4.3"
|
|
67
71
|
},
|
|
68
72
|
"author": "harveyzhang",
|
|
69
73
|
"repository": {
|