huhaa-myskills 0.2.13 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -375
- package/bin/huhaa-myskills.mjs +194 -12
- package/{scripts → build}/verify.mjs +3 -1
- package/docs/Frontend-Spec.md +1149 -0
- package/docs/assets/layout-wireframe.png +0 -0
- package/docs/assets/theme-reference.png +0 -0
- package/docs/scan_rules.md +172 -0
- package/package.json +33 -9
- package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
- package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
- package/packages/scanner/src/icon/brand-map.mjs +84 -0
- package/packages/scanner/src/icon/icon-extractor.mjs +246 -0
- package/packages/scanner/src/index.mjs +107 -82
- package/packages/scanner/src/types.d.ts +18 -0
- package/packages/scanner/test/icon.test.mjs +61 -0
- package/packages/scanner/test/scanner.test.mjs +37 -41
- package/packages/scanner/test/skill-adapter.test.mjs +70 -0
- package/packages/scanner/tests/directory-skill.test.mjs +93 -0
- package/packages/scanner/tests/scanner-integration.test.mjs +139 -0
- package/packages/server/src/index.mjs +164 -12
- package/packages/server/src/labels.mjs +62 -0
- package/packages/server/test/server.test.mjs +1 -1
- package/packages/web/README.md +33 -2
- package/packages/web/dist/assets/index-BCY2cRaO.js +175 -0
- package/packages/web/dist/assets/index-DALKjJPi.css +1 -0
- package/packages/web/dist/favicon-16x16.png +0 -0
- package/packages/web/dist/favicon-192x192.png +0 -0
- package/packages/web/dist/favicon-32x32.png +0 -0
- package/packages/web/dist/favicon-512x512.png +0 -0
- package/packages/web/dist/favicon-source.svg +5 -0
- package/packages/web/dist/index.html +23 -9
- package/packages/web/index.html +22 -8
- package/packages/web/package-lock.json +2989 -0
- package/packages/web/package.json +21 -8
- package/packages/web/postcss.config.js +6 -0
- package/packages/web/public/favicon-16x16.png +0 -0
- package/packages/web/public/favicon-192x192.png +0 -0
- package/packages/web/public/favicon-32x32.png +0 -0
- package/packages/web/public/favicon-512x512.png +0 -0
- package/packages/web/public/favicon-source.svg +5 -0
- package/packages/web/src/App.test.ts +40 -0
- package/packages/web/src/App.tsx +177 -0
- package/packages/web/src/components/ComingSoon.tsx +14 -0
- package/packages/web/src/components/layout/Sidebar.test.tsx +74 -0
- package/packages/web/src/components/layout/Sidebar.tsx +114 -0
- package/packages/web/src/components/layout/Topbar.tsx +74 -0
- package/packages/web/src/components/ui/ActionButton.tsx +48 -0
- package/packages/web/src/components/ui/button.tsx +48 -0
- package/packages/web/src/components/ui/card.tsx +45 -0
- package/packages/web/src/components/views/DashboardView.tsx +122 -0
- package/packages/web/src/components/views/OtherSkillsView.test.tsx +126 -0
- package/packages/web/src/components/views/OtherSkillsView.tsx +306 -0
- package/packages/web/src/components/views/SettingsView.tsx +95 -0
- package/packages/web/src/components/views/SkillDetail.test.tsx +57 -0
- package/packages/web/src/components/views/SkillDetail.tsx +81 -0
- package/packages/web/src/components/views/SkillsView.test.ts +28 -0
- package/packages/web/src/components/views/SkillsView.tsx +226 -0
- package/packages/web/src/hooks/QUICK_REFERENCE.md +165 -0
- package/packages/web/src/hooks/README.md +378 -0
- package/packages/web/src/hooks/__tests__/useOtherSkills.test.ts +422 -0
- package/packages/web/src/hooks/__tests__/useSkillIcons.test.ts +272 -0
- package/packages/web/src/hooks/useLiveReload.ts +20 -0
- package/packages/web/src/hooks/useOtherSkills.ts +387 -0
- package/packages/web/src/hooks/useSkillIcons.ts +248 -0
- package/packages/web/src/hooks/useTheme.ts +36 -0
- package/packages/web/src/index.css +161 -0
- package/packages/web/src/lib/api.test.ts +47 -0
- package/packages/web/src/lib/api.ts +77 -0
- package/packages/web/src/lib/cn.ts +7 -0
- package/packages/web/src/lib/editors.test.ts +30 -0
- package/packages/web/src/lib/editors.ts +70 -0
- package/packages/web/src/lib/markdown.test.ts +27 -0
- package/packages/web/src/lib/markdown.ts +14 -0
- package/packages/web/src/main.tsx +13 -0
- package/packages/web/src/types/index.ts +62 -0
- package/packages/web/src/types/other-skill.ts +88 -0
- package/packages/web/src/types/skill.ts +136 -0
- package/packages/web/src/types.ts +47 -0
- package/packages/web/src/vite-env.d.ts +1 -0
- package/packages/web/tailwind.config.ts +66 -0
- package/packages/web/tsconfig.json +28 -0
- package/packages/web/vite.config.ts +36 -0
- package/packages/web/vitest.config.ts +21 -0
- package/packages/web/vitest.setup.ts +1 -0
- package/scripts/activate-data-sources.sh +223 -0
- package/scripts/brand-completion.py +586 -0
- package/scripts/init-nextjs-project.sh +421 -0
- package/scripts/install-and-sync.sh +209 -0
- package/scripts/start-dev.sh +72 -0
- package/docs/GUIDE.md +0 -190
- package/docs/PLAN.md +0 -359
- package/docs/RULES.md +0 -329
- package/docs/RUNBOOK-myskills.md +0 -258
- package/docs/SYNC-SKILLS.md +0 -259
- package/docs/releases/README.md +0 -47
- package/docs/releases/v0.1.0.md +0 -256
- package/docs/releases/v0.1.1.md +0 -297
- package/docs/releases/v0.1.2.md +0 -207
- package/docs/releases/v0.1.3.md +0 -269
- package/docs/todo/v0.1.2.md +0 -98
- package/docs/todo/v0.1.3.md +0 -40
- package/docs/todo/v0.1.4.md +0 -36
- package/docs/todo/v0.1.5.md +0 -41
- package/docs/todo/v0.1.6.md +0 -43
- package/docs/todo/v0.1.7.md +0 -38
- package/docs/todo/v0.1.8.md +0 -36
- package/docs/todo/v0.1.9.md +0 -54
- package/packages/web/dist/assets/index-BPepSb3q.js +0 -36
- package/packages/web/dist/assets/index-CIGkgT5w.css +0 -1
- package/packages/web/src/App.vue +0 -483
- package/packages/web/src/components/AppTree.vue +0 -230
- package/packages/web/src/components/DirectoryTree.vue +0 -263
- package/packages/web/src/components/SkillTree.vue +0 -208
- package/packages/web/src/lib/api.js +0 -43
- package/packages/web/src/main.js +0 -6
- package/packages/web/src/stores/i18n.js +0 -161
- package/packages/web/src/stores/skills.js +0 -287
- package/packages/web/src/styles.css +0 -365
- package/packages/web/vite.config.js +0 -17
- /package/{scripts → build}/build-web.mjs +0 -0
- /package/{scripts → build}/prepare-publish.mjs +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# 技能扫描规则(Scan Rules)
|
|
2
|
+
|
|
3
|
+
> 本文档定义 HuHaa-MySkills 的技能扫描规范,是稳定的工程规则,供扫描器(`packages/scanner`)与服务端(`packages/server`)遵循。
|
|
4
|
+
> 版本:v3.0 · 上次更新:2026-07-01
|
|
5
|
+
|
|
6
|
+
设计参考开源项目 **Pearcleaner**(macOS 清理工具,Swift/SwiftUI)的「软件扫描逻辑」与「图标获取展示」,在 Node.js + Fastify + React 架构下等价落地。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## R1 扫描根目录
|
|
11
|
+
|
|
12
|
+
- 支持多个技能根目录:`roots: string[]`。
|
|
13
|
+
- `~` 展开为 `$HOME`。
|
|
14
|
+
- 每个 root 必须存在,否则返回 400。
|
|
15
|
+
- glob 返回精确且无重复的文件列表。
|
|
16
|
+
|
|
17
|
+
## R2 目录名称大小写不敏感 ⭐
|
|
18
|
+
|
|
19
|
+
- 技能目录可能命名为 `skills` / `Skills` / `SKILLS` / `sKiLLs`,扫描器接受所有变体。
|
|
20
|
+
- 按优先级返回第一个存在的目录。
|
|
21
|
+
- 文件名 `SKILL.md` 保持**大小写敏感**(`skill.md` 不匹配)。
|
|
22
|
+
|
|
23
|
+
## R3 文件匹配与递归扫描
|
|
24
|
+
|
|
25
|
+
- 使用 `fast-glob` 递归匹配 `**/SKILL.md`,文件名精确大小写。
|
|
26
|
+
- 每个文件仅读取一次;多个 root 命中同一文件时按 `filePath` 去重。
|
|
27
|
+
- 扫描采用分块并发(见 R7),但返回列表按路径字典序稳定排序,保证幂等。
|
|
28
|
+
|
|
29
|
+
## R4 Frontmatter 解析
|
|
30
|
+
|
|
31
|
+
- frontmatter 必须以 `---` 开始和结束;YAML 无效时返回 400。
|
|
32
|
+
- `name` / `description` 必填,缺失时返回 400。
|
|
33
|
+
- 其余字段缺失时用默认值或 null。
|
|
34
|
+
|
|
35
|
+
支持字段:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
---
|
|
39
|
+
name: string # 必须
|
|
40
|
+
description: string # 必须
|
|
41
|
+
category?: string
|
|
42
|
+
brand?: string # 图标解析主要依据(如 cursor / vs-code / claude-code)
|
|
43
|
+
source?: string # 图标解析次要依据
|
|
44
|
+
tier?: 'tool' | 'directory' | 'other'
|
|
45
|
+
tags?: string[]
|
|
46
|
+
icon?: string # 可选:显式指定图标,优先级最高
|
|
47
|
+
# - "app:com.todesktop.230313mzl4w4u92"(指定 Bundle ID)
|
|
48
|
+
# - "emoji:🤖"(强制 emoji)
|
|
49
|
+
# - "url:https://..."(远程/本地图标)
|
|
50
|
+
---
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## R5 返回数据格式
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
SkillItem {
|
|
57
|
+
id, name, description, filePath,
|
|
58
|
+
category?, brand?, tier?, tags?,
|
|
59
|
+
iconUrl?, // 真实图标端点,如 "/api/icons/cursor?size=64";无真实图标时为空
|
|
60
|
+
iconFallback?, // emoji 兜底值,如 "🤖"
|
|
61
|
+
createdAt?, updatedAt?
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ScanResult {
|
|
65
|
+
skills, total, limit, offset, roots, scannedAt, duration
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
排序:按 category 分组后按 name 字典序,同分类内 tier 优先。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## R6 真实工具/编辑器图标获取 ⭐
|
|
74
|
+
|
|
75
|
+
技能列表展示其所属工具/编辑器的**真实应用图标**,对标 Pearcleaner 的 `AppInfoUtils.fetchAppIcon(...)`。
|
|
76
|
+
|
|
77
|
+
### R6.1 品牌 → 应用映射表
|
|
78
|
+
|
|
79
|
+
维护 `brand/source → 候选 Bundle ID + 候选应用名` 的映射表(`packages/scanner/src/icon/brand-map.mjs`)。无对应 GUI 应用的品牌(如 `hermes`、`mcp`)按 R6.5 回退 emoji。
|
|
80
|
+
|
|
81
|
+
### R6.2 应用定位
|
|
82
|
+
|
|
83
|
+
按优先级定位 `.app`:
|
|
84
|
+
|
|
85
|
+
1. **Bundle ID 精确定位**(最快最准):`mdfind "kMDItemCFBundleIdentifier == '<bundleId>'"`
|
|
86
|
+
2. **应用名扫描**:遍历 `/Applications`、`~/Applications`、`/System/Applications`,`.app` 目录名大小写不敏感匹配
|
|
87
|
+
3. 命中多个时取修改时间最新者
|
|
88
|
+
|
|
89
|
+
### R6.3 图标提取(Node.js 等价 `NSWorkspace.icon(forFile:)`)
|
|
90
|
+
|
|
91
|
+
对定位到的 `<App>.app`:
|
|
92
|
+
|
|
93
|
+
1. 读 `Contents/Info.plist` 的 `CFBundleIconFile`(`plutil -extract CFBundleIconFile raw`)
|
|
94
|
+
2. 拼出 `Contents/Resources/<IconFile>.icns`(缺省兜底:目录下首个 `.icns`,优先含 `icon` 的文件)
|
|
95
|
+
3. 用系统内置 `sips` 转 PNG:
|
|
96
|
+
```bash
|
|
97
|
+
sips -s format png -Z 64 "<icns>" --out "<cachePng>"
|
|
98
|
+
```
|
|
99
|
+
支持尺寸 `32 / 64 / 128` 三档。
|
|
100
|
+
|
|
101
|
+
### R6.4 图标缓存
|
|
102
|
+
|
|
103
|
+
- 缓存目录:`~/.config/huhaa-myskills/icon-cache/`(受 `HUHAA_HOME` / `XDG_CONFIG_HOME` 影响)
|
|
104
|
+
- 文件名:`{appSlug}-{size}.png`
|
|
105
|
+
- **失效策略**:源 `.app` 的 mtime 新于缓存 PNG 时重建
|
|
106
|
+
- 进程内维护 `brand → appPath` / `brand:size → pngPath` 内存索引,避免重复 `sips`
|
|
107
|
+
|
|
108
|
+
### R6.5 图标优先级与降级链
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
1. frontmatter.icon 显式指定(app:/emoji:/url:) —— 最高优先级
|
|
112
|
+
2. 真实应用图标(R6.1–R6.4 成功)
|
|
113
|
+
3. emoji 映射表(BRAND_ICONS / CATEGORY_ICONS) —— 兜底降级
|
|
114
|
+
4. 通用占位图标 —— 最终兜底
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
找不到 app 时静默降级,不报错。
|
|
118
|
+
|
|
119
|
+
### R6.6 图标服务 API
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
GET /api/icons/:brand?size=64
|
|
123
|
+
→ 200 image/png (命中真实图标,cache-control: public, max-age=86400)
|
|
124
|
+
→ 404 JSON { ok:false, fallback:true, brand } (无对应 app,前端回退 emoji)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`size` 会被 clamp 到 `[32, 128]`,非法值回退 64。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## R7 扫描性能优化 ⭐
|
|
132
|
+
|
|
133
|
+
对标 Pearcleaner 的「并发扫描 + 两阶段加载 + Spotlight 元数据」。
|
|
134
|
+
|
|
135
|
+
### R7.1 两阶段加载
|
|
136
|
+
|
|
137
|
+
- **阶段 1(mini,快)**:返回轻量行(`id / name / description / category / brand / iconUrl / iconFallback`),前端立即渲染列表。
|
|
138
|
+
- **阶段 2(full,补全)**:后台补齐 `tags / links / preview / frontmatter 全字段 / 时间戳`,前端合并升级。
|
|
139
|
+
|
|
140
|
+
服务端通过 `stage=mini|full` 区分;前端先取 mini 快速渲染,再后台取 full 升级。图标 `<img loading="lazy">` 惰性加载。
|
|
141
|
+
|
|
142
|
+
### R7.2 并发分块扫描
|
|
143
|
+
|
|
144
|
+
- `fast-glob` 拿到文件列表后,用有界并发池(默认 = CPU 核数,范围 `[4, 16]`)并发解析。
|
|
145
|
+
- 每个 worker 顺序读文件、解析 frontmatter,避免一次性打开过多文件句柄。
|
|
146
|
+
- 结果合并后按路径字典序排序(保证 R3 幂等)。
|
|
147
|
+
|
|
148
|
+
### R7.3 Spotlight 元数据(可选)
|
|
149
|
+
|
|
150
|
+
- 文件时间戳优先用 `mdls -name kMDItemFSCreationDate -name kMDItemFSContentChangeDate`(批量,每批 50)。
|
|
151
|
+
- Spotlight 不可用时回退 `fs.stat` 的 mtime。
|
|
152
|
+
- 默认关闭,通过 `useSpotlight=1` 开启,失败静默回退。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 系统依赖与兼容性
|
|
157
|
+
|
|
158
|
+
- 依赖 macOS 内置命令:`sips` / `mdfind` / `plutil` / `mdls`(均无需额外安装)。
|
|
159
|
+
- 非 macOS 环境:图标提取直接按 R6.5 降级为 emoji,扫描功能不受影响。
|
|
160
|
+
- `iconUrl` / `iconFallback` 为可选字段,旧客户端不受影响;无真实图标时行为等同 emoji 模式。
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 附录:v2.0 规则原文(存档)
|
|
165
|
+
|
|
166
|
+
改版前 v2.0 的 R1–R5 定义,语义在 v3.0 中沿用:
|
|
167
|
+
|
|
168
|
+
- **R1 扫描根目录**:支持多个技能根目录,`~` 展开为 `$HOME`,每个 root 必须存在(否则 400),glob 返回精确且无重复的文件列表。
|
|
169
|
+
- **R2 目录名称大小写不敏感**:`Skills` / `SKILLS` / `skills` / `sKiLLs` 均可匹配,按优先级返回第一个存在的目录;文件名 `SKILL.md` 大小写敏感。
|
|
170
|
+
- **R3 文件匹配与递归扫描**:`fast-glob` 递归匹配 `**/SKILL.md`,每个文件仅读一次,返回按路径字典序排序。
|
|
171
|
+
- **R4 Frontmatter 解析**:frontmatter 以 `---` 开始和结束;YAML 无效时 400;`name`/`description` 必填。
|
|
172
|
+
- **R5 返回数据格式**:`SkillItem { id, name, description, filePath, category?, brand?, tier?, tags?, createdAt?, updatedAt? }`;`ScanResult { skills, total, limit, offset, roots, scannedAt, duration }`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huhaa-myskills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local skill / plugin / MCP aggregation hub. Browse all your AI agent skills at http://localhost:11520.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"bin/",
|
|
24
|
+
"build/",
|
|
24
25
|
"config/",
|
|
25
26
|
"packages/",
|
|
26
27
|
"scripts/",
|
|
@@ -30,31 +31,54 @@
|
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
33
|
"start": "node bin/huhaa-myskills.mjs start",
|
|
34
|
+
"stop": "node bin/huhaa-myskills.mjs stop",
|
|
35
|
+
"restart": "node bin/huhaa-myskills.mjs restart",
|
|
33
36
|
"scan": "node bin/huhaa-myskills.mjs scan",
|
|
34
37
|
"stats": "node bin/huhaa-myskills.mjs stats",
|
|
35
38
|
"duplicates": "node bin/huhaa-myskills.mjs duplicates",
|
|
36
39
|
"init": "node bin/huhaa-myskills.mjs init",
|
|
37
40
|
"purge": "node bin/huhaa-myskills.mjs purge",
|
|
38
|
-
"
|
|
39
|
-
"dev
|
|
41
|
+
"uninstall": "node bin/huhaa-myskills.mjs uninstall",
|
|
42
|
+
"dev": "cd packages/web && npm run dev",
|
|
43
|
+
"dev-bg": "bash scripts/start-dev.sh",
|
|
40
44
|
"sync": "node bin/huhaa-myskills.mjs sync",
|
|
41
|
-
"build:web": "node
|
|
45
|
+
"build:web": "node build/build-web.mjs",
|
|
46
|
+
"typecheck:web": "tsc --noEmit -p packages/web/tsconfig.json",
|
|
47
|
+
"test:web": "vitest run --root packages/web",
|
|
48
|
+
"prepack": "node build/build-web.mjs",
|
|
42
49
|
"test": "node --test packages/scanner/test/*.test.mjs packages/server/test/*.test.mjs",
|
|
43
|
-
"verify": "node
|
|
44
|
-
"release": "npm version patch && git push
|
|
50
|
+
"verify": "node build/verify.mjs",
|
|
51
|
+
"release": "npm version patch && git push"
|
|
45
52
|
},
|
|
46
53
|
"dependencies": {
|
|
47
54
|
"chokidar": "^4.0.3",
|
|
48
55
|
"fast-glob": "^3.3.2",
|
|
49
56
|
"fastify": "^5.0.0",
|
|
57
|
+
"google-translate-api-x": "^10.7.3",
|
|
50
58
|
"yaml": "^2.6.0"
|
|
51
59
|
},
|
|
52
60
|
"devDependencies": {
|
|
53
|
-
"@
|
|
61
|
+
"@testing-library/dom": "^10.4.1",
|
|
62
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
63
|
+
"@testing-library/react": "^16.3.2",
|
|
64
|
+
"@types/markdown-it": "^14.1.2",
|
|
65
|
+
"@types/react": "^18.3.12",
|
|
66
|
+
"@types/react-dom": "^18.3.1",
|
|
67
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
68
|
+
"autoprefixer": "^10.4.20",
|
|
69
|
+
"class-variance-authority": "^0.7.1",
|
|
70
|
+
"clsx": "^2.1.1",
|
|
54
71
|
"fuse.js": "^7.0.0",
|
|
72
|
+
"jsdom": "^29.1.1",
|
|
73
|
+
"lucide-react": "^0.460.0",
|
|
55
74
|
"markdown-it": "^14.1.0",
|
|
56
|
-
"
|
|
75
|
+
"postcss": "^8.4.49",
|
|
76
|
+
"react": "^18.3.1",
|
|
77
|
+
"react-dom": "^18.3.1",
|
|
78
|
+
"tailwind-merge": "^2.5.5",
|
|
79
|
+
"tailwindcss": "^3.4.15",
|
|
80
|
+
"typescript": "^5.6.3",
|
|
57
81
|
"vite": "^6.0.0",
|
|
58
|
-
"
|
|
82
|
+
"vitest": "^2.1.9"
|
|
59
83
|
}
|
|
60
84
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// directory-skill adapter — scan SKILL.md files from custom directories.
|
|
2
|
+
//
|
|
3
|
+
// This adapter scans user-specified directories (via directorySkillPaths in
|
|
4
|
+
// sources.yaml) for SKILL.md files. Each matched file is parsed as a skill
|
|
5
|
+
// and tagged with tier='directory' and dirName={parent directory name}.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// import { scanDirectorySkills } from './adapters/directory-skill.mjs';
|
|
9
|
+
// const result = await scanDirectorySkills({
|
|
10
|
+
// paths: ['~/Project/HuHaa-MySkills/packages/skills', '~/Work/custom-skills'],
|
|
11
|
+
// limits,
|
|
12
|
+
// });
|
|
13
|
+
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import fg from 'fast-glob';
|
|
16
|
+
import {
|
|
17
|
+
expandRoots,
|
|
18
|
+
classifyRoot,
|
|
19
|
+
readFileSafe,
|
|
20
|
+
parseFrontmatter,
|
|
21
|
+
inferBrand,
|
|
22
|
+
deriveDescription,
|
|
23
|
+
makePreview,
|
|
24
|
+
sha1Id,
|
|
25
|
+
} from '../utils.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Scan custom directory paths for SKILL.md files.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} opts
|
|
31
|
+
* @param {string[]} opts.paths - base dirs to scan (with ~ expansion)
|
|
32
|
+
* @param {string[]} [opts.globs] - optional glob patterns (default: glob matching SKILL.md)
|
|
33
|
+
* @param {object} [opts.limits] - { maxFiles, maxFileBytes }
|
|
34
|
+
* @returns {Promise}
|
|
35
|
+
*/
|
|
36
|
+
export async function scanDirectorySkills(opts) {
|
|
37
|
+
const {
|
|
38
|
+
paths = [],
|
|
39
|
+
globs = ['**/SKILL.md'],
|
|
40
|
+
limits = { maxFiles: 5000, maxFileBytes: 1024 * 1024 },
|
|
41
|
+
} = opts;
|
|
42
|
+
|
|
43
|
+
if (!paths || !paths.length) {
|
|
44
|
+
return { items: [], stats: { source: 'directory', available: false, files: 0 } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const expanded = await expandRoots(paths);
|
|
48
|
+
if (!expanded.length) {
|
|
49
|
+
return { items: [], stats: { source: 'directory', available: false, files: 0 } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Collect file paths from all directories and globs
|
|
53
|
+
const files = [];
|
|
54
|
+
for (const root of expanded) {
|
|
55
|
+
for (const globPattern of globs) {
|
|
56
|
+
const found = await fg(globPattern, {
|
|
57
|
+
cwd: root,
|
|
58
|
+
absolute: true,
|
|
59
|
+
onlyFiles: true,
|
|
60
|
+
dot: true,
|
|
61
|
+
followSymbolicLinks: true,
|
|
62
|
+
deep: 10,
|
|
63
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
for (const f of found) {
|
|
67
|
+
// Avoid duplicates
|
|
68
|
+
if (!files.some(file => file.abs === f)) {
|
|
69
|
+
files.push({ abs: f, root });
|
|
70
|
+
}
|
|
71
|
+
if (files.length >= limits.maxFiles) break;
|
|
72
|
+
}
|
|
73
|
+
if (files.length >= limits.maxFiles) break;
|
|
74
|
+
}
|
|
75
|
+
if (files.length >= limits.maxFiles) break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parse each SKILL.md file
|
|
79
|
+
const items = files.map(({ abs, root }) =>
|
|
80
|
+
parseDirectorySkill({ abs, root, limits })
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
items,
|
|
85
|
+
stats: {
|
|
86
|
+
source: 'directory',
|
|
87
|
+
available: true,
|
|
88
|
+
files: items.length,
|
|
89
|
+
roots: expanded,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse a single SKILL.md file from a custom directory.
|
|
96
|
+
*/
|
|
97
|
+
function parseDirectorySkill({ abs, root, limits }) {
|
|
98
|
+
const { text, truncated, mtime, error } = readFileSafe(abs, limits.maxFileBytes);
|
|
99
|
+
const id = sha1Id(abs);
|
|
100
|
+
const rel = path.relative(root, abs);
|
|
101
|
+
|
|
102
|
+
// Directory name: the parent directory of SKILL.md
|
|
103
|
+
// e.g. ~/Project/skills/auth-flow/SKILL.md → dirName = 'auth-flow'
|
|
104
|
+
const dirName = path.basename(path.dirname(abs));
|
|
105
|
+
|
|
106
|
+
// Category from path: if the structure is /root/category/skill-name/SKILL.md,
|
|
107
|
+
// extract category from the first level above root.
|
|
108
|
+
const relDir = path.dirname(rel);
|
|
109
|
+
const category = relDir && relDir !== '.' ? relDir.split('/')[0] : undefined;
|
|
110
|
+
|
|
111
|
+
if (error) {
|
|
112
|
+
return baseDirectoryItem({
|
|
113
|
+
abs,
|
|
114
|
+
id,
|
|
115
|
+
dirName,
|
|
116
|
+
category,
|
|
117
|
+
mtime,
|
|
118
|
+
raw: '',
|
|
119
|
+
parseError: error,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (truncated) {
|
|
124
|
+
return baseDirectoryItem({
|
|
125
|
+
abs,
|
|
126
|
+
id,
|
|
127
|
+
dirName,
|
|
128
|
+
category,
|
|
129
|
+
mtime,
|
|
130
|
+
raw: '',
|
|
131
|
+
parseError: `file > ${limits.maxFileBytes} bytes, skipped`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { data: fm, body, parseError } = parseFrontmatter(text);
|
|
136
|
+
|
|
137
|
+
const name = (fm.name || dirName).toString().trim();
|
|
138
|
+
const title = (fm.title || fm.name || dirName).toString().trim();
|
|
139
|
+
const description = deriveDescription(fm, body);
|
|
140
|
+
|
|
141
|
+
const triggers = collectTriggers(fm);
|
|
142
|
+
const tags = collectTags(fm);
|
|
143
|
+
const links = collectLinks(fm);
|
|
144
|
+
|
|
145
|
+
const item = {
|
|
146
|
+
id,
|
|
147
|
+
kind: 'skill',
|
|
148
|
+
source: 'directory',
|
|
149
|
+
editor: 'Custom Skills',
|
|
150
|
+
name,
|
|
151
|
+
title: title !== name ? title : undefined,
|
|
152
|
+
description,
|
|
153
|
+
category,
|
|
154
|
+
triggers: triggers.length ? triggers : undefined,
|
|
155
|
+
tags: tags.length ? tags : undefined,
|
|
156
|
+
paths: {
|
|
157
|
+
abs,
|
|
158
|
+
rel,
|
|
159
|
+
rootKind: classifyRoot(abs),
|
|
160
|
+
},
|
|
161
|
+
preview: makePreview(body),
|
|
162
|
+
raw: text,
|
|
163
|
+
links: links.length ? links : undefined,
|
|
164
|
+
updatedAt: new Date(mtime).toISOString(),
|
|
165
|
+
// Tier 2: Directory-based skills
|
|
166
|
+
tier: 'directory',
|
|
167
|
+
dirName,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Brand inference from content
|
|
171
|
+
item.brand = inferBrand({
|
|
172
|
+
name: item.name,
|
|
173
|
+
description: item.description,
|
|
174
|
+
category: item.category,
|
|
175
|
+
raw: body,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (parseError) item.parseError = parseError;
|
|
179
|
+
return item;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function baseDirectoryItem({ abs, id, dirName, category, mtime, raw, parseError }) {
|
|
183
|
+
return {
|
|
184
|
+
id,
|
|
185
|
+
kind: 'skill',
|
|
186
|
+
source: 'directory',
|
|
187
|
+
editor: 'Custom Skills',
|
|
188
|
+
name: dirName,
|
|
189
|
+
description: undefined,
|
|
190
|
+
category,
|
|
191
|
+
paths: {
|
|
192
|
+
abs,
|
|
193
|
+
rootKind: classifyRoot(abs),
|
|
194
|
+
},
|
|
195
|
+
preview: '',
|
|
196
|
+
raw,
|
|
197
|
+
updatedAt: new Date(mtime).toISOString(),
|
|
198
|
+
tier: 'directory',
|
|
199
|
+
dirName,
|
|
200
|
+
parseError,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function collectTriggers(fm) {
|
|
205
|
+
const out = [];
|
|
206
|
+
const candidates = [fm.triggers, fm.aliases, fm.when_to_use];
|
|
207
|
+
for (const c of candidates) {
|
|
208
|
+
if (!c) continue;
|
|
209
|
+
if (Array.isArray(c)) {
|
|
210
|
+
out.push(...c.filter(x => typeof x === 'string').map(x => x.trim()));
|
|
211
|
+
} else if (typeof c === 'string') {
|
|
212
|
+
out.push(c.trim());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return [...new Set(out)];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function collectTags(fm) {
|
|
219
|
+
const v = fm.tags;
|
|
220
|
+
if (!v) return [];
|
|
221
|
+
if (Array.isArray(v)) return v.filter(x => typeof x === 'string');
|
|
222
|
+
if (typeof v === 'string') return v.split(',').map(s => s.trim()).filter(Boolean);
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function collectLinks(fm) {
|
|
227
|
+
const out = [];
|
|
228
|
+
if (Array.isArray(fm.links)) {
|
|
229
|
+
for (const l of fm.links) {
|
|
230
|
+
if (l && typeof l === 'object' && l.url) {
|
|
231
|
+
out.push({ label: l.label || l.url, url: l.url });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (typeof fm.url === 'string') {
|
|
236
|
+
out.push({ label: 'docs', url: fm.url });
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|