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.
- package/CHANGELOG.md +29 -1
- package/bin/cli.js +137 -112
- package/package.json +9 -1
- package/skills/analysis/git-cleanup/SKILL.md +1 -1
- package/skills/analysis/skill-analyzer/SKILL.md +1 -1
- package/skills/data-extraction/vision-extract/SKILL.md +154 -0
- package/skills/design/sync-design-html/SKILL.md +1 -0
- package/skills/harness/diataxis-docs/SKILL.md +1 -0
- package/skills/harness/dir-manage/SKILL.md +113 -0
- package/skills/harness/dir-manage/references/built-in/diataxis.md +88 -0
- package/skills/harness/dir-manage/references/built-in/role-based.md +108 -0
- package/skills/harness/dir-manage/references/methodology-spec.md +98 -0
- package/skills/harness/full-stack-debug-env/SKILL.md +1 -0
- package/skills/harness/git-workflow-init/SKILL.md +1 -1
- package/skills/harness/migrate-specs/SKILL.md +184 -0
- package/skills/meta/contribute-skill/SKILL.md +1 -1
- package/skills/meta/npm-release/SKILL.md +178 -0
- package/skills/meta/skill-publish/SKILL.md +171 -0
- package/skills/superpowers-fork/brainstorming/SKILL.md +1 -1
- package/skills/superpowers-fork/executing-plans/SKILL.md +1 -1
- package/skills/superpowers-fork/systematic-debugging/SKILL.md +1 -1
- package/skills/superpowers-fork/using-git-worktrees/SKILL.md +1 -1
- package/skills/superpowers-fork/writing-plans/SKILL.md +1 -1
- package/skills/task/pm-task-dispatch/SKILL.md +2 -1
- package/skills/task/task-close/SKILL.md +2 -1
- package/skills/web-fetch/article-fetcher/SKILL.md +2 -5
- package/skills/web-fetch/url-extract/SKILL.md +218 -0
- package/skills/web-fetch/url-extract/platforms/SKILL.claude.md +31 -0
- package/skills/web-fetch/url-extract/platforms/SKILL.codex.md +17 -0
- package/skills/web-fetch/url-extract/platforms/SKILL.hermes.md +17 -0
- package/skills/web-fetch/url-extract/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/web-fetch/url-extract/references/article_utils.py +250 -0
- package/skills/web-fetch/url-extract/references/core-flow.md +188 -0
- package/skills/web-fetch/url-extract/references/file-format.md +73 -0
- package/skills/web-fetch/url-extract/scripts/dedup_check.py +41 -0
- package/skills/web-fetch/url-extract/scripts/playwright_web.py +172 -0
- package/skills/web-fetch/url-extract/scripts/playwright_xcom.py +221 -0
- package/skills/web-fetch/url-extract/scripts/validate_article.py +36 -0
- package/skills/web-fetch/url-extract/vars.json +17 -0
- package/skills/workflow/add-todo/SKILL.md +205 -0
- package/skills/writing/capture-insight/SKILL.md +112 -0
- package/skills/writing/mermaid-diagram/SKILL.md +1 -6
- 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.
|
|
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
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
830
|
-
|
|
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=
|
|
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: `
|
|
894
|
+
input: `install 安装 / 重新安装\nuninstall 卸载并清理文件`,
|
|
841
895
|
encoding: 'utf8',
|
|
842
896
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
843
897
|
})
|
|
844
|
-
if (!
|
|
898
|
+
if (!actionResult.stdout.trim()) {
|
|
845
899
|
console.log(chalk.dim(' · Cancelled'))
|
|
846
900
|
break
|
|
847
901
|
}
|
|
848
|
-
|
|
902
|
+
action = actionResult.stdout.trim().startsWith('uninstall') ? 'uninstall' : 'install'
|
|
903
|
+
}
|
|
904
|
+
}
|
|
849
905
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
875
|
-
skillSummary =
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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:
|
|
1023
|
+
input: targetChoices2.map(c => c.name).join('\n') + '\nall — all targets',
|
|
970
1024
|
encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
|
|
971
1025
|
})
|
|
972
|
-
if (!
|
|
973
|
-
const
|
|
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
|
-
|
|
976
|
-
const
|
|
977
|
-
'--
|
|
978
|
-
'--header=
|
|
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:
|
|
1036
|
+
input: 'user — 所有项目 (~/.{target}/skills/)\nproject — 仅当前项目 (.{target}/skills/)',
|
|
983
1037
|
encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
|
|
984
1038
|
})
|
|
985
|
-
if (!
|
|
986
|
-
const
|
|
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
|
-
//
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
|
1081
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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: "
|
|
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 文档管理
|