harveyz-skill 0.9.0 → 0.11.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/bin/cli.js +160 -132
  3. package/bin/preview.mjs +7 -5
  4. package/lib/bundles.js +4 -3
  5. package/lib/targets.js +1 -1
  6. package/package.json +9 -1
  7. package/skills/analysis/git-cleanup/SKILL.md +1 -1
  8. package/skills/analysis/skill-analyzer/SKILL.md +1 -1
  9. package/skills/data-extraction/vision-extract/SKILL.md +154 -0
  10. package/skills/design/sync-design-html/SKILL.md +1 -0
  11. package/skills/harness/diataxis-docs/SKILL.md +1 -0
  12. package/skills/harness/dir-manage/SKILL.md +113 -0
  13. package/skills/harness/dir-manage/references/built-in/diataxis.md +88 -0
  14. package/skills/harness/dir-manage/references/built-in/role-based.md +108 -0
  15. package/skills/harness/dir-manage/references/methodology-spec.md +98 -0
  16. package/skills/harness/full-stack-debug-env/SKILL.md +1 -0
  17. package/skills/harness/git-workflow-init/SKILL.md +1 -1
  18. package/skills/harness/migrate-specs/SKILL.md +184 -0
  19. package/skills/meta/contribute-skill/SKILL.md +1 -1
  20. package/skills/meta/npm-release/SKILL.md +202 -0
  21. package/skills/meta/skill-publish/SKILL.md +171 -0
  22. package/skills/superpowers-fork/brainstorming/SKILL.md +1 -1
  23. package/skills/superpowers-fork/executing-plans/SKILL.md +1 -1
  24. package/skills/superpowers-fork/systematic-debugging/SKILL.md +1 -1
  25. package/skills/superpowers-fork/using-git-worktrees/SKILL.md +1 -1
  26. package/skills/superpowers-fork/writing-plans/SKILL.md +1 -1
  27. package/skills/task/pm-task-dispatch/SKILL.md +2 -1
  28. package/skills/task/task-close/SKILL.md +2 -1
  29. package/skills/web-fetch/article-fetcher/SKILL.md +2 -5
  30. package/skills/web-fetch/article-fetcher/references/__pycache__/article_utils.cpython-314.pyc +0 -0
  31. package/skills/web-fetch/url-extract/SKILL.md +218 -0
  32. package/skills/web-fetch/url-extract/platforms/SKILL.claude.md +30 -0
  33. package/skills/web-fetch/url-extract/platforms/SKILL.codex.md +19 -0
  34. package/skills/web-fetch/url-extract/platforms/SKILL.hermes.md +19 -0
  35. package/skills/web-fetch/url-extract/references/__pycache__/article_utils.cpython-314.pyc +0 -0
  36. package/skills/web-fetch/url-extract/references/article_utils.py +250 -0
  37. package/skills/web-fetch/url-extract/references/core-flow.md +188 -0
  38. package/skills/web-fetch/url-extract/references/file-format.md +73 -0
  39. package/skills/web-fetch/url-extract/scripts/dedup_check.py +41 -0
  40. package/skills/web-fetch/url-extract/scripts/playwright_web.py +172 -0
  41. package/skills/web-fetch/url-extract/scripts/playwright_xcom.py +221 -0
  42. package/skills/web-fetch/url-extract/scripts/validate_article.py +36 -0
  43. package/skills/web-fetch/url-extract/vars.json +12 -0
  44. package/skills/workflow/add-todo/SKILL.md +205 -0
  45. package/skills/writing/capture-insight/SKILL.md +112 -0
  46. package/skills/writing/mermaid-diagram/SKILL.md +1 -6
  47. package/skills-index.json +17 -5
  48. package/tools/p-launch/__pycache__/p_launch.cpython-314.pyc +0 -0
  49. package/tools/p-launch/tests/__pycache__/test_p_launch.cpython-314-pytest-9.0.2.pyc +0 -0
package/CHANGELOG.md CHANGED
@@ -7,7 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.9.0] - 2026-05-28
10
+ ## [0.11.0] - 2026-06-09
11
+
12
+ ### Added
13
+ - `hskill list`:新增 Bundle 列并按 bundle 名称排序(替换旧的分组标题格式);`hskill status` skills 列表同步增加 Bundle 列
14
+
15
+ ### Changed
16
+ - `hskill` 内部重构:平台列表从 `SKILL_TARGETS` 单一来源派生,消除 `bundles.js` / `preview.mjs` 中的硬编码数组
17
+
18
+ ### Fixed
19
+ - `url-extract`:移除 `SKILL_DIR` 用户配置变量
20
+ - `preview`:安装状态侧边栏补全 openclaw、hermes 平台(USER LEVEL)
21
+ - `npm-release`:中间步骤跳过 push 操作,最终统一给出推送 + 发布指令清单
22
+
23
+ ## [0.10.0] - 2026-06-08
24
+
25
+ ### Added
26
+ - `url-extract` skill(`data-extraction` bundle):跨平台 URL 存档工具,支持 Claude Code / Codex / Hermes,含 Playwright 抓取、SQLite 存储、VAULT_PATH / SKILL_DIR / CHROME_PROFILE 配置
27
+ - `vision-extract` skill(`data-extraction` bundle):从 hermes-skills 贡献的视觉数据提取 skill
28
+ - `skill-publish` skill(`meta` bundle):检查 skill 格式合规性与 skills-index.json 注册状态
29
+ - `npm-release` skill(`meta` bundle):完整 npm 发布工作流(版本号 → CHANGELOG → 分支 → tag → publish)
30
+ - `migrate-specs` skill(`harness` bundle):将旧 spec 文档迁移为 Diataxis 结构
31
+ - `dir-manage` skill(`document` bundle):从 writing-agent 贡献的目录管理 skill
32
+ - `capture-insight` skill(`writing` bundle):从 writing-agent 贡献的洞察捕获 skill
33
+ - `add-todo` skill(`workflow` bundle):从 harveyz-skill workflow 贡献的 TODO 追踪 skill
34
+ - Diataxis 结构文档:cache、config、bundle、info 模块的参考文档与指南
35
+
36
+ ### Changed
37
+ - `hskill` 安装交互优化:改为两步式(先选 target,再选 scope),更符合操作直觉
38
+ - `url-extract`:通用 SKILL.md + 各平台 patch 文件,支持跨平台复用
39
+
40
+ ### Fixed
41
+ - installer:`--force` 时按 `uninstallPaths` 精确清理,支持版本感知 venv 升级
42
+ - `url-extract`:以 `source_url` 为主键,支持存量 DB schema 迁移
43
+ - `url-extract`:命令注入安全加固,usability 改进
44
+ - SKILL.md frontmatter 跨所有 skill 规范化
45
+
46
+ ## [0.9.0] - 2026-05-29
11
47
 
12
48
  ### Added
13
49
  - `hskill uninstall <tool>` 命令:清理 binary、Python 模块、tool.json、venv 及 zshrc snippet
@@ -22,6 +58,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
58
  ### Changed
23
59
  - p-launch `tool.json` 新增 `uninstallPaths`(`p-launch-venv`)和 `configPaths`(`~/.config/p-launch`)
24
60
 
61
+ ### Fixed
62
+ - installer:`--force` 重装时根据 `uninstallPaths` 精确清理旧版本文件,避免残留
63
+ - installer:`--force` 重装时正确清理 venv,支持版本感知升级
64
+ - tool 版本对比逻辑:upgrade 前先读取已安装版本,跳过同版本重复安装
65
+
25
66
  ## [0.8.1] - 2026-05-24
26
67
 
27
68
  ### Fixed
package/bin/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  resolveSkills, resolveSkillsByName, resolveTools, resolveToolsByName,
13
13
  TOOL_BUNDLE_CHOICES,
14
14
  } from '../lib/bundles.js'
15
- import { buildTargetChoices, resolveTargets, TARGETS } from '../lib/targets.js'
15
+ import { buildTargetChoices, resolveTargets, TARGETS, USER_ONLY_TARGETS } from '../lib/targets.js'
16
16
  import { installSkills, installTools, installHooks, installHooksForTarget, uninstallHook, uninstallTool, uninstallSkill } from '../lib/installer.js'
17
17
 
18
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -158,29 +158,31 @@ if (subcommand === 'update') {
158
158
 
159
159
  // ── List ─────────────────────────────────────────────────────────────────────
160
160
  if (subcommand === 'list') {
161
- const { bundleMeta, skills, tools = [] } = require('../skills-index.json')
162
- const byBundle = {}
163
- for (const s of skills) {
164
- if (!byBundle[s.bundle]) byBundle[s.bundle] = []
165
- byBundle[s.bundle].push(s.path)
166
- }
161
+ const { skills, tools = [] } = require('../skills-index.json')
162
+ const sorted = [...skills].sort((a, b) => a.bundle.localeCompare(b.bundle) || a.path.split('/').pop().localeCompare(b.path.split('/').pop()))
167
163
  if (jsonFlag) {
168
- const bundles = {}
169
- for (const [name, paths] of Object.entries(byBundle)) {
170
- bundles[name] = { description: bundleMeta[name] ?? name, skills: paths }
171
- }
172
- console.log(JSON.stringify({ bundles, tools: tools.map(t => t.name) }, null, 2))
164
+ console.log(JSON.stringify({
165
+ skills: sorted.map(s => ({ name: s.path.split('/').pop(), path: s.path, bundle: s.bundle })),
166
+ tools: tools.map(t => t.name),
167
+ }, null, 2))
173
168
  process.exit(0)
174
169
  }
175
- for (const [name, paths] of Object.entries(byBundle)) {
176
- console.log(chalk.bold(name) + ' — ' + (bundleMeta[name] ?? name))
177
- for (const p of paths) console.log(' ' + p)
170
+ const nw = Math.max(...sorted.map(s => s.path.split('/').pop().length), 4)
171
+ const bw = Math.max(...sorted.map(s => s.bundle.length), 6)
172
+ const sep = chalk.dim(' ' + '─'.repeat(nw + bw + 4))
173
+ console.log('')
174
+ console.log(' ' + chalk.bold('NAME'.padEnd(nw)) + ' ' + chalk.bold('BUNDLE'))
175
+ console.log(sep)
176
+ for (const s of sorted) {
177
+ console.log(' ' + s.path.split('/').pop().padEnd(nw) + ' ' + chalk.dim(s.bundle))
178
178
  }
179
179
  if (tools.length > 0) {
180
180
  console.log('')
181
- console.log(chalk.bold('shell tools:'))
181
+ console.log(' ' + chalk.bold('SHELL TOOLS'))
182
+ console.log(sep)
182
183
  for (const t of tools) console.log(' ' + t.name)
183
184
  }
185
+ console.log('')
184
186
  process.exit(0)
185
187
  }
186
188
 
@@ -208,11 +210,11 @@ if (subcommand === 'status' || subcommand === 'outdated') {
208
210
  const skillRows = skillItems.map(s => {
209
211
  const inst = checkInstalled(s.skillName, s.version ?? '—')
210
212
  return {
211
- name: s.skillName, version: s.version ?? '—',
213
+ name: s.skillName, bundle: s.bundle ?? '—', version: s.version ?? '—',
212
214
  userStatus: scopeSummary(inst.user), projectStatus: scopeSummary(inst.project),
213
215
  userDetail: inst.user, projectDetail: inst.project,
214
216
  }
215
- })
217
+ }).sort((a, b) => a.bundle.localeCompare(b.bundle) || a.name.localeCompare(b.name))
216
218
  const toolRows = toolItems.map(t => {
217
219
  const inst = checkToolInstalled(t.toolName, t.srcPath)
218
220
  return { name: t.toolName, version: t.version ?? '—', ...inst }
@@ -285,15 +287,16 @@ if (subcommand === 'status' || subcommand === 'outdated') {
285
287
  const allVers = [...skillRows, ...toolRows].map(r => r.version)
286
288
  const nw = Math.max(...allNames.map(n => n.length), 4)
287
289
  const vw = Math.max(...allVers.map(v => v.length), 7)
288
- const sep = chalk.dim(' ' + '─'.repeat(nw + vw + 20))
290
+ const bw = Math.max(...skillRows.map(r => r.bundle.length), 6)
291
+ const sep = chalk.dim(' ' + '─'.repeat(nw + vw + bw + 22))
289
292
 
290
293
  console.log('')
291
294
  console.log(' ' + chalk.bold('SKILLS') + chalk.dim(` — ${skillRows.length} available`))
292
295
  console.log(sep)
293
- console.log(' ' + ''.padEnd(nw + vw + 3) + chalk.dim('user project'))
296
+ console.log(' ' + ''.padEnd(nw + vw + bw + 5) + chalk.dim('user project'))
294
297
  for (const r of skillRows) {
295
298
  const u = icon(r.userStatus), p = icon(r.projectStatus)
296
- console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + u + ' ' + p)
299
+ console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + chalk.dim(r.bundle.padEnd(bw)) + ' ' + u + ' ' + p)
297
300
  }
298
301
 
299
302
  console.log('')
@@ -594,6 +597,69 @@ const scopeArg = scopeIdx !== -1 ? installArgs[scopeIdx + 1] : undefined
594
597
 
595
598
  const TOOL_BUNDLE_VALUES = new Set(TOOL_BUNDLE_CHOICES.map(c => c.value))
596
599
 
600
+ // ── Two-step target→scope selector ───────────────────────────────────────────
601
+ const ALL_SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
602
+
603
+ function selectTargetThenScope() {
604
+ // Step 1: platform (target)
605
+ const targetInput = [
606
+ 'claude ~/.claude/skills/',
607
+ 'cursor ~/.cursor/skills/',
608
+ 'codex ~/.codex/skills/',
609
+ 'openclaw ~/.openclaw/skills/',
610
+ 'hermes ~/.hermes/skills/',
611
+ 'all all 5 targets',
612
+ ].join('\n')
613
+
614
+ const targetResult = spawnSync('fzf', [
615
+ '--multi',
616
+ '--prompt= › ',
617
+ '--header= 安装到 · tab 多选 · enter 确认 · esc 取消',
618
+ '--layout=reverse',
619
+ '--border=rounded',
620
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
621
+ ], {
622
+ input: targetInput,
623
+ encoding: 'utf8',
624
+ stdio: ['pipe', 'pipe', 'inherit'],
625
+ })
626
+
627
+ if (!targetResult.stdout.trim()) return null
628
+
629
+ const rawTargets = targetResult.stdout.trim().split('\n')
630
+ .map(l => l.trim().split(/\s+/)[0])
631
+ const expandedTargets = rawTargets.includes('all') ? ALL_SKILL_TARGETS : rawTargets
632
+
633
+ // Step 2: scope (user/project) — skip if all selected targets are user-only
634
+ const allUserOnly = expandedTargets.every(t => USER_ONLY_TARGETS.has(t))
635
+ let scope = 'user'
636
+ if (!allUserOnly) {
637
+ const scopeResult = spawnSync('fzf', [
638
+ '--prompt= › ',
639
+ '--header= 安装范围 · enter 确认 · esc 取消',
640
+ '--layout=reverse',
641
+ '--border=rounded',
642
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
643
+ ], {
644
+ input: 'user — 所有项目共享 (~/.{target}/skills/)\nproject — 仅当前项目 (.{target}/skills/)',
645
+ encoding: 'utf8',
646
+ stdio: ['pipe', 'pipe', 'inherit'],
647
+ })
648
+ if (!scopeResult.stdout.trim()) return null
649
+ scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
650
+ }
651
+
652
+ // Build final list — openclaw/hermes always resolve to user scope
653
+ const seen = new Set()
654
+ const result = []
655
+ for (const t of expandedTargets) {
656
+ const effectiveScope = USER_ONLY_TARGETS.has(t) ? 'user' : scope
657
+ const key = `${effectiveScope}/${t}`
658
+ if (!seen.has(key)) { seen.add(key); result.push({ scope: effectiveScope, target: t }) }
659
+ }
660
+ return result // [{ scope, target }]
661
+ }
662
+
597
663
  function requireFzf() {
598
664
  const probe = spawnSync('fzf', ['--version'], { encoding: 'utf8' })
599
665
  if (probe.error || probe.status !== 0) {
@@ -804,77 +870,66 @@ try {
804
870
  if (!skillItems.length && !toolItems.length && !hookItems.length) continue
805
871
 
806
872
  // ── Action selection (install / uninstall) ─────────────────────────────
873
+ // Skip the prompt when nothing is installed yet — default to install.
807
874
  let action = 'install'
808
875
  if (process.env.HSKILL_TEST_ACTION) {
809
876
  action = process.env.HSKILL_TEST_ACTION
810
877
  } else {
811
- const actionResult = spawnSync('fzf', [
812
- '--prompt= › ',
813
- '--header= Action · enter 确认 · esc 取消',
814
- '--layout=reverse',
815
- '--border=rounded',
816
- '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
817
- ], {
818
- input: `install 安装 / 重新安装\nuninstall 卸载并清理文件`,
819
- encoding: 'utf8',
820
- stdio: ['pipe', 'pipe', 'inherit'],
821
- })
822
- if (!actionResult.stdout.trim()) {
823
- console.log(chalk.dim(' · Cancelled'))
824
- break
825
- }
826
- action = actionResult.stdout.trim().startsWith('uninstall') ? 'uninstall' : 'install'
827
- }
878
+ const anyInstalled =
879
+ skillItems.some(s => {
880
+ const inst = checkInstalled(s.skillName, s.version ?? '—')
881
+ return scopeSummary(inst.user) !== 'none' || scopeSummary(inst.project) !== 'none'
882
+ }) ||
883
+ toolItems.some(t => checkToolInstalled(t.toolName, t.srcPath).status !== 'none') ||
884
+ hookItems.some(h => {
885
+ const inst = checkHookInstalled(h.name)
886
+ return inst.user.status !== 'none' || inst.project.status !== 'none'
887
+ })
828
888
 
829
- if (action === 'install') {
830
- let skillSummary = null
831
- if (skillItems.length > 0) {
832
- // Scope selection
833
- const scopeResult = spawnSync('fzf', [
889
+ if (anyInstalled) {
890
+ const actionResult = spawnSync('fzf', [
834
891
  '--prompt= › ',
835
- '--header= Scope · enter 确认 · esc 取消',
892
+ '--header= Action · enter 确认 · esc 取消',
836
893
  '--layout=reverse',
837
894
  '--border=rounded',
838
895
  '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
839
896
  ], {
840
- input: `user — ~/.claude/skills/ (所有项目共享)\nproject — .claude/skills/ (仅当前项目)`,
897
+ input: `install 安装 / 重新安装\nuninstall 卸载并清理文件`,
841
898
  encoding: 'utf8',
842
899
  stdio: ['pipe', 'pipe', 'inherit'],
843
900
  })
844
- if (!scopeResult.stdout.trim()) {
901
+ if (!actionResult.stdout.trim()) {
845
902
  console.log(chalk.dim(' · Cancelled'))
846
903
  break
847
904
  }
848
- const scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
905
+ action = actionResult.stdout.trim().startsWith('uninstall') ? 'uninstall' : 'install'
906
+ }
907
+ }
849
908
 
850
- // Target selection
851
- const targetChoices = buildTargetChoices(scope)
852
- const targetInput = targetChoices.map(c => c.name).join('\n') + '\nall — all tools'
853
- const targetResult = spawnSync('fzf', [
854
- '--multi',
855
- '--prompt= › ',
856
- '--header= Install to · tab 多选 · enter 确认 · esc 取消',
857
- '--layout=reverse',
858
- '--border=rounded',
859
- '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
860
- ], {
861
- input: targetInput,
862
- encoding: 'utf8',
863
- stdio: ['pipe', 'pipe', 'inherit'],
864
- })
865
- if (!targetResult.stdout.trim()) {
909
+ if (action === 'install') {
910
+ let skillSummary = null
911
+ if (skillItems.length > 0) {
912
+ // Combined scope+target selection (one step instead of two)
913
+ const selectedST = selectTargetThenScope()
914
+ if (!selectedST) {
866
915
  console.log(chalk.dim(' · Cancelled'))
867
916
  break
868
917
  }
869
- const selectedTargets = targetResult.stdout.trim().split('\n')
870
- .map(l => l.trim().split(/\s+/)[0])
871
918
 
872
- if (selectedTargets.length > 0) {
919
+ // Group by scope so we make one installSkills call per scope
920
+ const byScope = {}
921
+ for (const { scope, target } of selectedST) {
922
+ if (!byScope[scope]) byScope[scope] = []
923
+ byScope[scope].push(target)
924
+ }
925
+
926
+ console.log('')
927
+ for (const [scope, selectedTargets] of Object.entries(byScope)) {
873
928
  const targets = resolveTargets(selectedTargets, scope)
874
- console.log('')
875
- skillSummary = await installSkills(skillItems, targets, forceFlag)
876
- console.log('')
929
+ const result = await installSkills(skillItems, targets, forceFlag)
930
+ skillSummary = skillSummary ? { ...skillSummary, ...result } : result
877
931
  }
932
+ console.log('')
878
933
  }
879
934
 
880
935
  let toolSummary = null
@@ -960,30 +1015,33 @@ try {
960
1015
  }
961
1016
 
962
1017
  for (const item of uSkillItems) {
963
- const scopeRes = spawnSync('fzf', [
964
- '--prompt= ',
965
- '--header= Uninstall from scope · enter 确认 · esc 取消',
1018
+ // Step 1: target (platform)
1019
+ const targetChoices2 = buildTargetChoices('user')
1020
+ const targetRes = spawnSync('fzf', [
1021
+ '--multi', '--prompt= › ',
1022
+ '--header= 从哪里卸载 · tab 多选 · enter 确认 · esc 取消',
966
1023
  '--layout=reverse', '--border=rounded',
967
1024
  '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
968
1025
  ], {
969
- input: `user — ~/.claude/skills/\nproject .claude/skills/`,
1026
+ input: targetChoices2.map(c => c.name).join('\n') + '\nall all targets',
970
1027
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
971
1028
  })
972
- if (!scopeRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
973
- const scope2 = scopeRes.stdout.trim().startsWith('project') ? 'project' : 'user'
1029
+ if (!targetRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
1030
+ const selTargets2 = targetRes.stdout.trim().split('\n').map(l => l.trim().split(/\s+/)[0])
974
1031
 
975
- const targetChoices2 = buildTargetChoices(scope2)
976
- const targetRes = spawnSync('fzf', [
977
- '--multi', '--prompt= › ',
978
- '--header= Uninstall from · tab 多选 · enter 确认',
1032
+ // Step 2: scope (user/project)
1033
+ const scopeRes = spawnSync('fzf', [
1034
+ '--prompt= › ',
1035
+ '--header= 卸载范围 · enter 确认 · esc 取消',
979
1036
  '--layout=reverse', '--border=rounded',
980
1037
  '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
981
1038
  ], {
982
- input: targetChoices2.map(c => c.name).join('\n') + '\nall all tools',
1039
+ input: 'user — 所有项目 (~/.{target}/skills/)\nproject 仅当前项目 (.{target}/skills/)',
983
1040
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
984
1041
  })
985
- if (!targetRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
986
- const selTargets2 = targetRes.stdout.trim().split('\n').map(l => l.trim().split(/\s+/)[0])
1042
+ if (!scopeRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
1043
+ const scope2 = scopeRes.stdout.trim().startsWith('project') ? 'project' : 'user'
1044
+
987
1045
  const targets2 = resolveTargets(selTargets2, scope2)
988
1046
  for (const { dir } of targets2) {
989
1047
  await uninstallSkill(item.skillName, dir)
@@ -1043,66 +1101,36 @@ try {
1043
1101
  // ── Install skills ──────────────────────────────────────────────────────────
1044
1102
  let skillSummary = null
1045
1103
  if (skillItems.length > 0) {
1046
- // Resolve scope
1047
- let scope = scopeArg ?? 'user'
1048
- if (!scopeArg && !targetArg) {
1049
- if (!process.stdout.isTTY) {
1050
- console.error(chalk.red(' ✗ Interactive scope selection requires a TTY. Use --scope user|project.'))
1051
- process.exit(1)
1052
- }
1053
- const scopeResult = spawnSync('fzf', [
1054
- '--prompt= › ',
1055
- '--header= Scope · enter 确认 · esc 取消',
1056
- '--layout=reverse',
1057
- '--border=rounded',
1058
- '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
1059
- ], {
1060
- input: `user — ~/.claude/skills/ (所有项目共享)\nproject — .claude/skills/ (仅当前项目)`,
1061
- encoding: 'utf8',
1062
- stdio: ['pipe', 'pipe', 'inherit'],
1063
- })
1064
- if (!scopeResult.stdout.trim()) {
1065
- console.log(chalk.dim(' · Cancelled'))
1066
- process.exit(0)
1067
- }
1068
- scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
1069
- }
1070
-
1071
- // Resolve target
1072
- let selectedTargets
1104
+ // When --target is given, use it directly (with --scope or default 'user').
1105
+ // When only --skill is given and no --target, use the combined selector.
1073
1106
  if (targetArg) {
1074
- selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex', 'openclaw', 'hermes'] : [targetArg]
1107
+ const scope = scopeArg ?? 'user'
1108
+ const selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex', 'openclaw', 'hermes'] : [targetArg]
1109
+ const targets = resolveTargets(selectedTargets, scope)
1110
+ console.log('')
1111
+ skillSummary = await installSkills(skillItems, targets, forceFlag)
1112
+ console.log('')
1075
1113
  } else {
1076
1114
  if (!process.stdout.isTTY) {
1077
1115
  console.error(chalk.red(' ✗ Interactive target selection requires a TTY. Use --target claude|cursor|codex|openclaw|hermes|all.'))
1078
1116
  process.exit(1)
1079
1117
  }
1080
- const targetChoices = buildTargetChoices(scope)
1081
- const targetInput = targetChoices.map(c => c.name).join('\n') + '\nall — all tools'
1082
- const targetResult = spawnSync('fzf', [
1083
- '--multi',
1084
- '--prompt= › ',
1085
- '--header= Install to · tab 多选 · enter 确认 · esc 取消',
1086
- '--layout=reverse',
1087
- '--border=rounded',
1088
- '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
1089
- ], {
1090
- input: targetInput,
1091
- encoding: 'utf8',
1092
- stdio: ['pipe', 'pipe', 'inherit'],
1093
- })
1094
- if (!targetResult.stdout.trim()) {
1118
+ const selectedST = selectTargetThenScope()
1119
+ if (!selectedST) {
1095
1120
  console.log(chalk.dim(' · Cancelled'))
1096
1121
  process.exit(0)
1097
1122
  }
1098
- selectedTargets = targetResult.stdout.trim().split('\n')
1099
- .map(l => l.trim().split(/\s+/)[0])
1100
- }
1101
-
1102
- if (selectedTargets.length > 0) {
1103
- const targets = resolveTargets(selectedTargets, scope)
1123
+ const byScope = {}
1124
+ for (const { scope, target } of selectedST) {
1125
+ if (!byScope[scope]) byScope[scope] = []
1126
+ byScope[scope].push(target)
1127
+ }
1104
1128
  console.log('')
1105
- skillSummary = await installSkills(skillItems, targets, forceFlag)
1129
+ for (const [scope, selectedTargets] of Object.entries(byScope)) {
1130
+ const targets = resolveTargets(selectedTargets, scope)
1131
+ const result = await installSkills(skillItems, targets, forceFlag)
1132
+ skillSummary = skillSummary ? { ...skillSummary, ...result } : result
1133
+ }
1106
1134
  console.log('')
1107
1135
  }
1108
1136
  }
package/bin/preview.mjs CHANGED
@@ -3,6 +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
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
9
 
@@ -72,10 +73,11 @@ function statusLine(version, status) {
72
73
  return ver + ' ' + D + '— not installed' + R
73
74
  }
74
75
 
75
- const targets = ['claude', 'cursor', 'codex']
76
+ const userTargets = SKILL_TARGETS
77
+ const projectTargets = SKILL_TARGETS.filter(t => !USER_ONLY_TARGETS.has(t))
76
78
  const home = os.homedir()
77
79
 
78
- function checkScope(dirFn) {
80
+ function checkScope(targets, dirFn) {
79
81
  return targets.map(t => {
80
82
  const ver = readVersion(path.join(dirFn(t), skillName))
81
83
  const status = ver === '—' ? 'none'
@@ -86,10 +88,10 @@ function checkScope(dirFn) {
86
88
  }
87
89
 
88
90
  const cwd = process.cwd()
89
- const userDetails = checkScope(t => path.join(home, `.${t}`, 'skills'))
91
+ const userDetails = checkScope(userTargets, t => path.join(home, `.${t}`, 'skills'))
90
92
  const projectDetails = cwd === home
91
- ? targets.map(t => ({ tool: t, version: '—', status: 'none' }))
92
- : checkScope(t => path.join(cwd, `.${t}`, 'skills'))
93
+ ? projectTargets.map(t => ({ tool: t, version: '—', status: 'none' }))
94
+ : checkScope(projectTargets, t => path.join(cwd, `.${t}`, 'skills'))
93
95
 
94
96
  console.log(B + skillName + R)
95
97
  console.log(D + 'available: ' + R + availableVersion)
package/lib/bundles.js CHANGED
@@ -3,7 +3,7 @@ import os from 'os'
3
3
  import { createRequire } from 'module'
4
4
  import path from 'path'
5
5
  import { fileURLToPath } from 'url'
6
- import { USER_ONLY_TARGETS } from './targets.js'
6
+ import { SKILL_TARGETS, USER_ONLY_TARGETS } from './targets.js'
7
7
 
8
8
  const require = createRequire(import.meta.url)
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -101,13 +101,14 @@ export function buildAllChoices() {
101
101
  return choices
102
102
  }
103
103
 
104
- // 所有 skill 展开为 { kind:'skill', skillName, srcPath } 列表(对应 all 选项)
104
+ // 所有 skill 展开为 { kind:'skill', skillName, bundle, srcPath } 列表(对应 all 选项)
105
105
  export function getAllSkillItems() {
106
106
  return skillDefs.map(skill => {
107
107
  const srcPath = path.join(skillsRoot, skill.path)
108
108
  return {
109
109
  kind: 'skill',
110
110
  skillName: skill.path.split('/').pop(),
111
+ bundle: skill.bundle,
111
112
  srcPath,
112
113
  version: readVersion(srcPath),
113
114
  }
@@ -132,7 +133,7 @@ export function getAllToolItems() {
132
133
  // 每个工具: { version: string, status: 'up-to-date'|'update'|'none' }
133
134
  export function checkInstalled(skillName, availableVersion) {
134
135
  const home = os.homedir()
135
- const targets = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
136
+ const targets = SKILL_TARGETS
136
137
 
137
138
  function checkScope(dirFn) {
138
139
  const result = {}
package/lib/targets.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import os from 'os'
2
2
  import path from 'path'
3
3
 
4
- const SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
4
+ export const SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
5
5
 
6
6
  export const USER_ONLY_TARGETS = new Set(['openclaw', 'hermes'])
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harveyz-skill",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Skill manager for Claude Code, Cursor, and Codex",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,10 +28,18 @@
28
28
  "skills/task/pm-task-dispatch/",
29
29
  "skills/task/task-close/",
30
30
  "skills/web-fetch/article-fetcher/",
31
+ "skills/web-fetch/url-extract/",
32
+ "skills/data-extraction/vision-extract/",
31
33
  "skills/writing/mermaid-diagram/",
32
34
  "skills/design/sync-design-html/",
33
35
  "skills/analysis/git-cleanup/",
34
36
  "skills/meta/contribute-skill/",
37
+ "skills/harness/migrate-specs/",
38
+ "skills/workflow/add-todo/",
39
+ "skills/harness/dir-manage/",
40
+ "skills/writing/capture-insight/",
41
+ "skills/meta/skill-publish/",
42
+ "skills/meta/npm-release/",
35
43
  "tools/p-launch/",
36
44
  "hooks/check-similar-branch/"
37
45
  ],
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: git-cleanup
3
- description: "梳理并清理本地 git 分支。规则匹配 + LLM 语义分析,分组确认后批量删除废弃分支。触发词:清理分支、branch cleanup、梳理分支、删除旧分支、整理分支"
3
+ description: "Tidy and clean up local git branches using rule matching and LLM semantic analysis, then batch-delete stale branches after grouped confirmation. Triggers: clean up branches, branch cleanup, tidy branches, delete old branches, organize branches."
4
4
  user_invocable: true
5
5
  version: "1.0.0"
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: skill-analyzer
3
- description: " Skill 仓库进行系统性分析。触发词:分析这个 skill 仓库、对这个 skill 仓库做系统性研究、输出 skill 仓库的分析报告、理解这个 skill 系统的设计意图"
3
+ description: "Performs systematic analysis of the skill repository. Triggers: analyze this skill repo, do a systematic study of the skill repo, output skill repo analysis report, understand the design intent of this skill system."
4
4
  user_invocable: true
5
5
  version: "1.0.0"
6
6
  ---