harveyz-skill 0.9.0 → 0.10.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 (43) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/bin/cli.js +137 -112
  3. package/package.json +9 -1
  4. package/skills/analysis/git-cleanup/SKILL.md +1 -1
  5. package/skills/analysis/skill-analyzer/SKILL.md +1 -1
  6. package/skills/data-extraction/vision-extract/SKILL.md +154 -0
  7. package/skills/design/sync-design-html/SKILL.md +1 -0
  8. package/skills/harness/diataxis-docs/SKILL.md +1 -0
  9. package/skills/harness/dir-manage/SKILL.md +113 -0
  10. package/skills/harness/dir-manage/references/built-in/diataxis.md +88 -0
  11. package/skills/harness/dir-manage/references/built-in/role-based.md +108 -0
  12. package/skills/harness/dir-manage/references/methodology-spec.md +98 -0
  13. package/skills/harness/full-stack-debug-env/SKILL.md +1 -0
  14. package/skills/harness/git-workflow-init/SKILL.md +1 -1
  15. package/skills/harness/migrate-specs/SKILL.md +184 -0
  16. package/skills/meta/contribute-skill/SKILL.md +1 -1
  17. package/skills/meta/npm-release/SKILL.md +178 -0
  18. package/skills/meta/skill-publish/SKILL.md +171 -0
  19. package/skills/superpowers-fork/brainstorming/SKILL.md +1 -1
  20. package/skills/superpowers-fork/executing-plans/SKILL.md +1 -1
  21. package/skills/superpowers-fork/systematic-debugging/SKILL.md +1 -1
  22. package/skills/superpowers-fork/using-git-worktrees/SKILL.md +1 -1
  23. package/skills/superpowers-fork/writing-plans/SKILL.md +1 -1
  24. package/skills/task/pm-task-dispatch/SKILL.md +2 -1
  25. package/skills/task/task-close/SKILL.md +2 -1
  26. package/skills/web-fetch/article-fetcher/SKILL.md +2 -5
  27. package/skills/web-fetch/url-extract/SKILL.md +218 -0
  28. package/skills/web-fetch/url-extract/platforms/SKILL.claude.md +31 -0
  29. package/skills/web-fetch/url-extract/platforms/SKILL.codex.md +17 -0
  30. package/skills/web-fetch/url-extract/platforms/SKILL.hermes.md +17 -0
  31. package/skills/web-fetch/url-extract/references/__pycache__/article_utils.cpython-314.pyc +0 -0
  32. package/skills/web-fetch/url-extract/references/article_utils.py +250 -0
  33. package/skills/web-fetch/url-extract/references/core-flow.md +188 -0
  34. package/skills/web-fetch/url-extract/references/file-format.md +73 -0
  35. package/skills/web-fetch/url-extract/scripts/dedup_check.py +41 -0
  36. package/skills/web-fetch/url-extract/scripts/playwright_web.py +172 -0
  37. package/skills/web-fetch/url-extract/scripts/playwright_xcom.py +221 -0
  38. package/skills/web-fetch/url-extract/scripts/validate_article.py +36 -0
  39. package/skills/web-fetch/url-extract/vars.json +17 -0
  40. package/skills/workflow/add-todo/SKILL.md +205 -0
  41. package/skills/writing/capture-insight/SKILL.md +112 -0
  42. package/skills/writing/mermaid-diagram/SKILL.md +1 -6
  43. package/skills-index.json +17 -5
package/CHANGELOG.md CHANGED
@@ -7,7 +7,30 @@ 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.10.0] - 2026-06-08
11
+
12
+ ### Added
13
+ - `url-extract` skill(`data-extraction` bundle):跨平台 URL 存档工具,支持 Claude Code / Codex / Hermes,含 Playwright 抓取、SQLite 存储、VAULT_PATH / SKILL_DIR / CHROME_PROFILE 配置
14
+ - `vision-extract` skill(`data-extraction` bundle):从 hermes-skills 贡献的视觉数据提取 skill
15
+ - `skill-publish` skill(`meta` bundle):检查 skill 格式合规性与 skills-index.json 注册状态
16
+ - `npm-release` skill(`meta` bundle):完整 npm 发布工作流(版本号 → CHANGELOG → 分支 → tag → publish)
17
+ - `migrate-specs` skill(`harness` bundle):将旧 spec 文档迁移为 Diataxis 结构
18
+ - `dir-manage` skill(`document` bundle):从 writing-agent 贡献的目录管理 skill
19
+ - `capture-insight` skill(`writing` bundle):从 writing-agent 贡献的洞察捕获 skill
20
+ - `add-todo` skill(`workflow` bundle):从 harveyz-skill workflow 贡献的 TODO 追踪 skill
21
+ - Diataxis 结构文档:cache、config、bundle、info 模块的参考文档与指南
22
+
23
+ ### Changed
24
+ - `hskill` 安装交互优化:改为两步式(先选 target,再选 scope),更符合操作直觉
25
+ - `url-extract`:通用 SKILL.md + 各平台 patch 文件,支持跨平台复用
26
+
27
+ ### Fixed
28
+ - installer:`--force` 时按 `uninstallPaths` 精确清理,支持版本感知 venv 升级
29
+ - `url-extract`:以 `source_url` 为主键,支持存量 DB schema 迁移
30
+ - `url-extract`:命令注入安全加固,usability 改进
31
+ - SKILL.md frontmatter 跨所有 skill 规范化
32
+
33
+ ## [0.9.0] - 2026-05-29
11
34
 
12
35
  ### Added
13
36
  - `hskill uninstall <tool>` 命令:清理 binary、Python 模块、tool.json、venv 及 zshrc snippet
@@ -22,6 +45,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
45
  ### Changed
23
46
  - p-launch `tool.json` 新增 `uninstallPaths`(`p-launch-venv`)和 `configPaths`(`~/.config/p-launch`)
24
47
 
48
+ ### Fixed
49
+ - installer:`--force` 重装时根据 `uninstallPaths` 精确清理旧版本文件,避免残留
50
+ - installer:`--force` 重装时正确清理 venv,支持版本感知升级
51
+ - tool 版本对比逻辑:upgrade 前先读取已安装版本,跳过同版本重复安装
52
+
25
53
  ## [0.8.1] - 2026-05-24
26
54
 
27
55
  ### 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))
@@ -594,6 +594,69 @@ const scopeArg = scopeIdx !== -1 ? installArgs[scopeIdx + 1] : undefined
594
594
 
595
595
  const TOOL_BUNDLE_VALUES = new Set(TOOL_BUNDLE_CHOICES.map(c => c.value))
596
596
 
597
+ // ── Two-step target→scope selector ───────────────────────────────────────────
598
+ const ALL_SKILL_TARGETS = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
599
+
600
+ function selectTargetThenScope() {
601
+ // Step 1: platform (target)
602
+ const targetInput = [
603
+ 'claude ~/.claude/skills/',
604
+ 'cursor ~/.cursor/skills/',
605
+ 'codex ~/.codex/skills/',
606
+ 'openclaw ~/.openclaw/skills/',
607
+ 'hermes ~/.hermes/skills/',
608
+ 'all all 5 targets',
609
+ ].join('\n')
610
+
611
+ const targetResult = spawnSync('fzf', [
612
+ '--multi',
613
+ '--prompt= › ',
614
+ '--header= 安装到 · tab 多选 · enter 确认 · esc 取消',
615
+ '--layout=reverse',
616
+ '--border=rounded',
617
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
618
+ ], {
619
+ input: targetInput,
620
+ encoding: 'utf8',
621
+ stdio: ['pipe', 'pipe', 'inherit'],
622
+ })
623
+
624
+ if (!targetResult.stdout.trim()) return null
625
+
626
+ const rawTargets = targetResult.stdout.trim().split('\n')
627
+ .map(l => l.trim().split(/\s+/)[0])
628
+ const expandedTargets = rawTargets.includes('all') ? ALL_SKILL_TARGETS : rawTargets
629
+
630
+ // Step 2: scope (user/project) — skip if all selected targets are user-only
631
+ const allUserOnly = expandedTargets.every(t => USER_ONLY_TARGETS.has(t))
632
+ let scope = 'user'
633
+ if (!allUserOnly) {
634
+ const scopeResult = spawnSync('fzf', [
635
+ '--prompt= › ',
636
+ '--header= 安装范围 · enter 确认 · esc 取消',
637
+ '--layout=reverse',
638
+ '--border=rounded',
639
+ '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
640
+ ], {
641
+ input: 'user — 所有项目共享 (~/.{target}/skills/)\nproject — 仅当前项目 (.{target}/skills/)',
642
+ encoding: 'utf8',
643
+ stdio: ['pipe', 'pipe', 'inherit'],
644
+ })
645
+ if (!scopeResult.stdout.trim()) return null
646
+ scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
647
+ }
648
+
649
+ // Build final list — openclaw/hermes always resolve to user scope
650
+ const seen = new Set()
651
+ const result = []
652
+ for (const t of expandedTargets) {
653
+ const effectiveScope = USER_ONLY_TARGETS.has(t) ? 'user' : scope
654
+ const key = `${effectiveScope}/${t}`
655
+ if (!seen.has(key)) { seen.add(key); result.push({ scope: effectiveScope, target: t }) }
656
+ }
657
+ return result // [{ scope, target }]
658
+ }
659
+
597
660
  function requireFzf() {
598
661
  const probe = spawnSync('fzf', ['--version'], { encoding: 'utf8' })
599
662
  if (probe.error || probe.status !== 0) {
@@ -804,77 +867,66 @@ try {
804
867
  if (!skillItems.length && !toolItems.length && !hookItems.length) continue
805
868
 
806
869
  // ── Action selection (install / uninstall) ─────────────────────────────
870
+ // Skip the prompt when nothing is installed yet — default to install.
807
871
  let action = 'install'
808
872
  if (process.env.HSKILL_TEST_ACTION) {
809
873
  action = process.env.HSKILL_TEST_ACTION
810
874
  } 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
- }
875
+ const anyInstalled =
876
+ skillItems.some(s => {
877
+ const inst = checkInstalled(s.skillName, s.version ?? '—')
878
+ return scopeSummary(inst.user) !== 'none' || scopeSummary(inst.project) !== 'none'
879
+ }) ||
880
+ toolItems.some(t => checkToolInstalled(t.toolName, t.srcPath).status !== 'none') ||
881
+ hookItems.some(h => {
882
+ const inst = checkHookInstalled(h.name)
883
+ return inst.user.status !== 'none' || inst.project.status !== 'none'
884
+ })
828
885
 
829
- if (action === 'install') {
830
- let skillSummary = null
831
- if (skillItems.length > 0) {
832
- // Scope selection
833
- const scopeResult = spawnSync('fzf', [
886
+ if (anyInstalled) {
887
+ const actionResult = spawnSync('fzf', [
834
888
  '--prompt= › ',
835
- '--header= Scope · enter 确认 · esc 取消',
889
+ '--header= Action · enter 确认 · esc 取消',
836
890
  '--layout=reverse',
837
891
  '--border=rounded',
838
892
  '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
839
893
  ], {
840
- input: `user — ~/.claude/skills/ (所有项目共享)\nproject — .claude/skills/ (仅当前项目)`,
894
+ input: `install 安装 / 重新安装\nuninstall 卸载并清理文件`,
841
895
  encoding: 'utf8',
842
896
  stdio: ['pipe', 'pipe', 'inherit'],
843
897
  })
844
- if (!scopeResult.stdout.trim()) {
898
+ if (!actionResult.stdout.trim()) {
845
899
  console.log(chalk.dim(' · Cancelled'))
846
900
  break
847
901
  }
848
- const scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
902
+ action = actionResult.stdout.trim().startsWith('uninstall') ? 'uninstall' : 'install'
903
+ }
904
+ }
849
905
 
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()) {
906
+ if (action === 'install') {
907
+ let skillSummary = null
908
+ if (skillItems.length > 0) {
909
+ // Combined scope+target selection (one step instead of two)
910
+ const selectedST = selectTargetThenScope()
911
+ if (!selectedST) {
866
912
  console.log(chalk.dim(' · Cancelled'))
867
913
  break
868
914
  }
869
- const selectedTargets = targetResult.stdout.trim().split('\n')
870
- .map(l => l.trim().split(/\s+/)[0])
871
915
 
872
- if (selectedTargets.length > 0) {
916
+ // Group by scope so we make one installSkills call per scope
917
+ const byScope = {}
918
+ for (const { scope, target } of selectedST) {
919
+ if (!byScope[scope]) byScope[scope] = []
920
+ byScope[scope].push(target)
921
+ }
922
+
923
+ console.log('')
924
+ for (const [scope, selectedTargets] of Object.entries(byScope)) {
873
925
  const targets = resolveTargets(selectedTargets, scope)
874
- console.log('')
875
- skillSummary = await installSkills(skillItems, targets, forceFlag)
876
- console.log('')
926
+ const result = await installSkills(skillItems, targets, forceFlag)
927
+ skillSummary = skillSummary ? { ...skillSummary, ...result } : result
877
928
  }
929
+ console.log('')
878
930
  }
879
931
 
880
932
  let toolSummary = null
@@ -960,30 +1012,33 @@ try {
960
1012
  }
961
1013
 
962
1014
  for (const item of uSkillItems) {
963
- const scopeRes = spawnSync('fzf', [
964
- '--prompt= ',
965
- '--header= Uninstall from scope · enter 确认 · esc 取消',
1015
+ // Step 1: target (platform)
1016
+ const targetChoices2 = buildTargetChoices('user')
1017
+ const targetRes = spawnSync('fzf', [
1018
+ '--multi', '--prompt= › ',
1019
+ '--header= 从哪里卸载 · tab 多选 · enter 确认 · esc 取消',
966
1020
  '--layout=reverse', '--border=rounded',
967
1021
  '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
968
1022
  ], {
969
- input: `user — ~/.claude/skills/\nproject .claude/skills/`,
1023
+ input: targetChoices2.map(c => c.name).join('\n') + '\nall all targets',
970
1024
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
971
1025
  })
972
- if (!scopeRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
973
- const scope2 = scopeRes.stdout.trim().startsWith('project') ? 'project' : 'user'
1026
+ if (!targetRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
1027
+ const selTargets2 = targetRes.stdout.trim().split('\n').map(l => l.trim().split(/\s+/)[0])
974
1028
 
975
- const targetChoices2 = buildTargetChoices(scope2)
976
- const targetRes = spawnSync('fzf', [
977
- '--multi', '--prompt= › ',
978
- '--header= Uninstall from · tab 多选 · enter 确认',
1029
+ // Step 2: scope (user/project)
1030
+ const scopeRes = spawnSync('fzf', [
1031
+ '--prompt= › ',
1032
+ '--header= 卸载范围 · enter 确认 · esc 取消',
979
1033
  '--layout=reverse', '--border=rounded',
980
1034
  '--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
981
1035
  ], {
982
- input: targetChoices2.map(c => c.name).join('\n') + '\nall all tools',
1036
+ input: 'user — 所有项目 (~/.{target}/skills/)\nproject 仅当前项目 (.{target}/skills/)',
983
1037
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
984
1038
  })
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])
1039
+ if (!scopeRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
1040
+ const scope2 = scopeRes.stdout.trim().startsWith('project') ? 'project' : 'user'
1041
+
987
1042
  const targets2 = resolveTargets(selTargets2, scope2)
988
1043
  for (const { dir } of targets2) {
989
1044
  await uninstallSkill(item.skillName, dir)
@@ -1043,66 +1098,36 @@ try {
1043
1098
  // ── Install skills ──────────────────────────────────────────────────────────
1044
1099
  let skillSummary = null
1045
1100
  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
1101
+ // When --target is given, use it directly (with --scope or default 'user').
1102
+ // When only --skill is given and no --target, use the combined selector.
1073
1103
  if (targetArg) {
1074
- selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex', 'openclaw', 'hermes'] : [targetArg]
1104
+ const scope = scopeArg ?? 'user'
1105
+ const selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex', 'openclaw', 'hermes'] : [targetArg]
1106
+ const targets = resolveTargets(selectedTargets, scope)
1107
+ console.log('')
1108
+ skillSummary = await installSkills(skillItems, targets, forceFlag)
1109
+ console.log('')
1075
1110
  } else {
1076
1111
  if (!process.stdout.isTTY) {
1077
1112
  console.error(chalk.red(' ✗ Interactive target selection requires a TTY. Use --target claude|cursor|codex|openclaw|hermes|all.'))
1078
1113
  process.exit(1)
1079
1114
  }
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()) {
1115
+ const selectedST = selectTargetThenScope()
1116
+ if (!selectedST) {
1095
1117
  console.log(chalk.dim(' · Cancelled'))
1096
1118
  process.exit(0)
1097
1119
  }
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)
1120
+ const byScope = {}
1121
+ for (const { scope, target } of selectedST) {
1122
+ if (!byScope[scope]) byScope[scope] = []
1123
+ byScope[scope].push(target)
1124
+ }
1104
1125
  console.log('')
1105
- skillSummary = await installSkills(skillItems, targets, forceFlag)
1126
+ for (const [scope, selectedTargets] of Object.entries(byScope)) {
1127
+ const targets = resolveTargets(selectedTargets, scope)
1128
+ const result = await installSkills(skillItems, targets, forceFlag)
1129
+ skillSummary = skillSummary ? { ...skillSummary, ...result } : result
1130
+ }
1106
1131
  console.log('')
1107
1132
  }
1108
1133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harveyz-skill",
3
- "version": "0.9.0",
3
+ "version": "0.10.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
  ---
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: vision-extract
3
+ description: "Use when extracting specific information from images (screenshots, photos, receipts, menus) via PaddleOCR + subagent filtering."
4
+ version: 1.0.0
5
+ user_invocable: true
6
+ author: Hermes Agent
7
+ license: MIT
8
+ platforms: [linux, macos, windows]
9
+ metadata:
10
+ hermes:
11
+ tags: [OCR, image, text-extraction, screenshots, vision, subagent]
12
+ related_skills: [ocr-and-documents]
13
+ ---
14
+
15
+ # Image Text Extraction: PaddleOCR + Subagent Filtering
16
+
17
+ ## When to Use
18
+
19
+ - User shares a screenshot, photo, or image and wants to extract **specific information** (not just "all text")
20
+ - User provides natural language requirements (e.g., "提取金额和日期", "找出总价", "列出所有菜单项及价格")
21
+ - Works for any image format: PNG, JPG, WEBP, BMP, TIFF, PDF pages, etc.
22
+
23
+ ## How It Works
24
+
25
+ Two-step pipeline:
26
+
27
+ 1. **Step 1 — PaddleOCR**: Extract ALL text from the image (raw, unfiltered)
28
+ 2. **Step 2 — Subagent**: Filter the OCR text based on the user's exact requirements and return only what they asked for
29
+
30
+ ## Step 1 — Run PaddleOCR
31
+
32
+ **IMPORTANT**: This skill uses PaddleOCR v3.x. The API differs significantly from v2.x. Do NOT use `show_log`, `use_angle_cls`, or `cls` parameters — they are invalid or deprecated.
33
+
34
+ ```python
35
+ from paddleocr import PaddleOCR
36
+
37
+ def ocr_full_text(image_path, lang='en'):
38
+ """Extract all text from an image using PaddleOCR v3.x."""
39
+ ocr = PaddleOCR(lang=lang) # v3: no show_log, no use_angle_cls
40
+ result = ocr.ocr(image_path)
41
+ if not result:
42
+ return ''
43
+ page = result[0]
44
+ rec_texts = page.get('rec_texts', [])
45
+ dt_polys = page.get('dt_polys', [])
46
+ rec_scores = page.get('rec_scores', [])
47
+ lines = []
48
+ for poly, text, score in zip(dt_polys, rec_texts, rec_scores):
49
+ lines.append(f'{text} | conf={score:.2f}')
50
+ return '\n'.join(lines)
51
+
52
+ text = ocr_full_text('/path/to/image.png')
53
+ ```
54
+
55
+ > **API differences from v2**: v3 returns `{'rec_texts': [...], 'dt_polys': [...], 'rec_scores': [...]}` per page instead of `[(bbox, (text, score)), ...]`. Do not use `result[0][0][1][0]` — it will raise `KeyError`.
56
+
57
+ ### Language options
58
+
59
+ | lang | Language |
60
+ |------|----------|
61
+ | `en` | English |
62
+ | `ch` | Chinese only |
63
+ | `ch+en` | Chinese + English mixed |
64
+ | `fr` | French |
65
+ | `de` | German |
66
+ | `ja` | Japanese |
67
+ | `ko` | Korean |
68
+
69
+ ### Large images
70
+
71
+ Downscale if >4000px to improve speed and accuracy:
72
+
73
+ ```python
74
+ from PIL import Image
75
+ img = Image.open('large.png')
76
+ img = img.resize((2000, int(2000 * img.height / img.width)))
77
+ img.save('resized.png')
78
+ ```
79
+
80
+ ## Step 2 — Subagent Filtering
81
+
82
+ After getting the full OCR text, spawn a subagent to extract **only what the user wants**:
83
+
84
+ ```python
85
+ delegate_task(
86
+ goal=f"""Here is the raw OCR text extracted from an image:
87
+
88
+ --- OCR RESULT START ---
89
+ {ocr_text}
90
+ --- OCR RESULT END ---
91
+
92
+ User wants: <natural language requirement>
93
+ Extract from the OCR result above only the information the user requested.
94
+ If the user asks for specific fields (e.g. "日期" or "total amount"), return those fields as a JSON object.
95
+ If the user asks to list items, return a structured list.
96
+ Return ONLY the filtered result, do not summarize the image or describe what it is.""",
97
+ toolsets=["terminal"]
98
+ )
99
+ ```
100
+
101
+ **Important**: The subagent uses `toolsets=["terminal"]` — it only sees the OCR text, not the image.
102
+
103
+ ## Full Example: Receipt Data Extraction
104
+
105
+ ```python
106
+ from paddleocr import PaddleOCR
107
+
108
+ # Step 1: OCR
109
+ ocr = PaddleOCR(lang='en')
110
+ result = ocr.ocr('/path/to/receipt.png')
111
+ page = result[0]
112
+ rec_texts = page.get('rec_texts', [])
113
+ dt_polys = page.get('dt_polys', [])
114
+ rec_scores = page.get('rec_scores', [])
115
+ ocr_text = '\n'.join(
116
+ f'{text} | conf={score:.2f}'
117
+ for text, score in zip(rec_texts, rec_scores)
118
+ )
119
+
120
+ # Step 2: Subagent filters
121
+ delegate_task(
122
+ goal=f"""OCR text from a receipt:
123
+ --- START ---
124
+ {ocr_text}
125
+ --- END ---
126
+
127
+ Extract:
128
+ - vendor_name: the store or company name
129
+ - date: the transaction date
130
+ - total_amount: the total amount paid (just the number)
131
+ - items: list of items purchased as {{"item": "...", "price": "..."}}
132
+
133
+ Return as JSON.""",
134
+ toolsets=["terminal"]
135
+ )
136
+ ```
137
+
138
+ ## Common Pitfalls
139
+
140
+ 1. **Wrong API (PaddleOCR v3)** — `use_angle_cls`, `show_log`, `cls=True`, and tuple-unpacking `line[1][0]` are all wrong for v3. Use `PaddleOCR(lang=lang)` with no extra params, then access `result[0].get('rec_texts', [])`.
141
+ 2. **First-run slow** — PaddleOCR downloads inference models (~300MB) on first OCR call. Models are cached in `~/.paddlex/`.
142
+ 3. **Low confidence on small text** — Text below ~10px in the image will have poor accuracy. For menu bar icons / very small UI elements, use the vision subagent instead.
143
+ 4. **Subagent receives empty text** — If PaddleOCR returns empty, fall back to a vision subagent directly on the image.
144
+ 5. **Chinese + English** — Use `lang='ch+en'` for mixed content.
145
+
146
+ ## Verification Checklist
147
+
148
+ - [ ] `python3 -c "from paddleocr import PaddleOCR; print('OK')"` runs without import error
149
+ - [ ] OCR returns non-empty `rec_texts` list for a test image
150
+ - [ ] Subagent goal contains the full OCR text (not just a path or summary)
151
+ - [ ] Subagent returns only filtered results per the user's requirement
152
+
153
+ ---
154
+
@@ -2,6 +2,7 @@
2
2
  name: sync-design-html
3
3
  description: Use after any UI source file change — detects changed view/component files via git diff and syncs them to high-fidelity HTML design backups. Maintains a manifest as the source-of-truth mapping between source files and HTML previews. Also invocable as a post-step from /ship or /review. Trigger when user says "sync design", "update HTML preview", "design changed", or after UI code edits.
4
4
  version: "3.0.0"
5
+ user_invocable: true
5
6
  ---
6
7
 
7
8
  # Sync Design HTML
@@ -2,6 +2,7 @@
2
2
  name: diataxis-docs
3
3
  version: "1.0.0"
4
4
  description: Use when creating, updating, or deleting any file under docs/ — including writing a new guide, updating reference content, adding an ADR, or removing an outdated page. Also use when searching docs before writing, to avoid duplicating existing content.
5
+ user_invocable: true
5
6
  ---
6
7
 
7
8
  # Diataxis 文档管理