skillhelper 0.4.2
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/LICENSE +21 -0
- package/README.md +105 -0
- package/bin/lib/atomic-write.mjs +72 -0
- package/bin/lib/paths.mjs +65 -0
- package/bin/lib/port.mjs +23 -0
- package/bin/skillhelper.mjs +720 -0
- package/build/build-site.mjs +35 -0
- package/build/build-web.mjs +29 -0
- package/build/pack-smoke.mjs +195 -0
- package/build/prepare-publish.mjs +3 -0
- package/build/verify.mjs +157 -0
- package/config/sources.example.yaml +126 -0
- package/docs/Frontend-Spec.md +1207 -0
- package/docs/scan_skills_rules.md +432 -0
- package/package.json +95 -0
- package/packages/scanner/package.json +15 -0
- package/packages/scanner/src/adapters/claude-agents.mjs +134 -0
- package/packages/scanner/src/adapters/claude-plugin.mjs +260 -0
- package/packages/scanner/src/adapters/codex-plugin.mjs +330 -0
- package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
- package/packages/scanner/src/adapters/file-docs.mjs +158 -0
- package/packages/scanner/src/adapters/hermes-plugin.mjs +149 -0
- package/packages/scanner/src/adapters/markdown-skill.mjs +229 -0
- package/packages/scanner/src/adapters/mcp-config.mjs +190 -0
- package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
- package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
- package/packages/scanner/src/adapters/tier1-editor-skills.mjs +278 -0
- package/packages/scanner/src/adapters/tier2-user-skills.mjs +218 -0
- package/packages/scanner/src/adapters/tier3-other-skills.mjs +225 -0
- package/packages/scanner/src/config/editor-tiers.mjs +191 -0
- package/packages/scanner/src/core/atomic-write.mjs +11 -0
- package/packages/scanner/src/core/descriptor.mjs +143 -0
- package/packages/scanner/src/core/path-hash.mjs +89 -0
- package/packages/scanner/src/core/registry.mjs +128 -0
- package/packages/scanner/src/hash/path-hash.mjs +8 -0
- package/packages/scanner/src/icon/brand-map.mjs +268 -0
- package/packages/scanner/src/icon/icon-extractor.mjs +466 -0
- package/packages/scanner/src/index.mjs +450 -0
- package/packages/scanner/src/types.d.ts +144 -0
- package/packages/scanner/src/utils.mjs +194 -0
- package/packages/server/package.json +15 -0
- package/packages/server/src/index.mjs +1071 -0
- package/packages/server/src/labels.mjs +64 -0
- package/packages/server/src/langdetect.mjs +34 -0
- package/packages/server/src/llm-client.mjs +42 -0
- package/packages/server/src/translate-cache.mjs +132 -0
- package/packages/server/src/translator.mjs +53 -0
- package/packages/web/README.md +36 -0
- package/packages/web/dist/assets/index-CGt04vt0.js +304 -0
- package/packages/web/dist/assets/index-CLSDoa3H.css +1 -0
- package/packages/web/dist/brand-icon.png +0 -0
- package/packages/web/dist/brand-logo.png +0 -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/favicon.ico +0 -0
- package/packages/web/dist/favicon.svg +5 -0
- package/packages/web/dist/icons/hermes-128.png +0 -0
- package/packages/web/dist/icons/hermes-192.png +0 -0
- package/packages/web/dist/icons/hermes-256.png +0 -0
- package/packages/web/dist/icons/hermes-32.png +0 -0
- package/packages/web/dist/icons/hermes-512.png +0 -0
- package/packages/web/dist/icons/hermes-64.png +0 -0
- package/packages/web/dist/icons/hermes.png +0 -0
- package/packages/web/dist/index.html +37 -0
- package/packages/web/dist/robots.txt +3 -0
- package/packages/web/dist/site.webmanifest +24 -0
- package/packages/web/public/brand-icon.png +0 -0
- package/packages/web/public/brand-logo.png +0 -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/public/favicon.ico +0 -0
- package/packages/web/public/favicon.svg +5 -0
- package/packages/web/public/icons/hermes-128.png +0 -0
- package/packages/web/public/icons/hermes-192.png +0 -0
- package/packages/web/public/icons/hermes-256.png +0 -0
- package/packages/web/public/icons/hermes-32.png +0 -0
- package/packages/web/public/icons/hermes-512.png +0 -0
- package/packages/web/public/icons/hermes-64.png +0 -0
- package/packages/web/public/icons/hermes.png +0 -0
- package/packages/web/public/robots.txt +3 -0
- package/packages/web/public/site.webmanifest +24 -0
- package/scripts/install-and-sync.sh +209 -0
- package/scripts/sync-skills.sh +605 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Shared scanner utilities — every adapter MUST use these for consistency.
|
|
2
|
+
//
|
|
3
|
+
// parseFrontmatter YAML frontmatter at top of markdown, tolerant
|
|
4
|
+
// sha1Id stable id from absolute path
|
|
5
|
+
// expandRoots glob-aware path resolution (~ / *)
|
|
6
|
+
// inferBrand keyword-match brand from text
|
|
7
|
+
// inferProduct extract product from skill name / category
|
|
8
|
+
// classifyRoot 'home' | 'project' | 'icloud'
|
|
9
|
+
// readFileSafe size-guarded read
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import YAML from 'yaml';
|
|
16
|
+
import fg from 'fast-glob';
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────── path / fs ────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export function expandTilde(p) {
|
|
21
|
+
if (!p) return p;
|
|
22
|
+
if (p === '~' || p.startsWith('~/')) {
|
|
23
|
+
return path.join(os.homedir(), p.slice(2));
|
|
24
|
+
}
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Expand a list of roots (may contain ~ and glob *) into actual existing dirs.
|
|
30
|
+
* Non-existing paths are silently dropped — callers report "unavailable" via
|
|
31
|
+
* stat checks separately.
|
|
32
|
+
*/
|
|
33
|
+
export async function expandRoots(roots) {
|
|
34
|
+
if (!roots || !roots.length) return [];
|
|
35
|
+
const out = new Set();
|
|
36
|
+
for (const r of roots) {
|
|
37
|
+
const expanded = expandTilde(r);
|
|
38
|
+
if (expanded.includes('*')) {
|
|
39
|
+
const matches = await fg(expanded, { onlyDirectories: true, absolute: true });
|
|
40
|
+
matches.forEach(m => out.add(m));
|
|
41
|
+
} else {
|
|
42
|
+
const abs = path.resolve(expanded);
|
|
43
|
+
if (fs.existsSync(abs)) out.add(abs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...out];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function classifyRoot(absPath) {
|
|
50
|
+
if (absPath.includes('/Library/Mobile Documents/')) return 'icloud';
|
|
51
|
+
if (absPath.startsWith(os.homedir() + '/Project/')) return 'project';
|
|
52
|
+
if (absPath.startsWith(os.homedir())) return 'home';
|
|
53
|
+
return 'project';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readFileSafe(abs, maxBytes = 1024 * 1024) {
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.statSync(abs);
|
|
59
|
+
if (stat.size > maxBytes) {
|
|
60
|
+
return { text: '', truncated: true, size: stat.size, mtime: stat.mtime };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
text: fs.readFileSync(abs, 'utf8'),
|
|
64
|
+
truncated: false,
|
|
65
|
+
size: stat.size,
|
|
66
|
+
mtime: stat.mtime,
|
|
67
|
+
};
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { text: '', error: e.message, size: 0, mtime: new Date(0) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────── id / hash ────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function sha1Id(absPath) {
|
|
76
|
+
return crypto.createHash('sha1').update(absPath).digest('hex').slice(0, 16);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────── frontmatter ──────────────────────────────
|
|
80
|
+
|
|
81
|
+
const FM_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse YAML frontmatter at the very top of a markdown file.
|
|
85
|
+
* Returns { data, body, i18n?, parseError? }. `data` is always an object (possibly
|
|
86
|
+
* empty). Body never has the frontmatter block.
|
|
87
|
+
*/
|
|
88
|
+
export function parseFrontmatter(text) {
|
|
89
|
+
if (!text || !text.startsWith('---')) {
|
|
90
|
+
return { data: {}, body: text || '' };
|
|
91
|
+
}
|
|
92
|
+
const m = text.match(FM_RE);
|
|
93
|
+
if (!m) {
|
|
94
|
+
return { data: {}, body: text };
|
|
95
|
+
}
|
|
96
|
+
const [, yamlBlock, body] = m;
|
|
97
|
+
try {
|
|
98
|
+
const data = YAML.parse(yamlBlock) || {};
|
|
99
|
+
|
|
100
|
+
// Extract i18n fields if present
|
|
101
|
+
const i18n = {};
|
|
102
|
+
for (const key of ['name_zh', 'description_zh', 'category_zh']) {
|
|
103
|
+
if (data[key]) i18n[key] = data[key];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
data: typeof data === 'object' && !Array.isArray(data) ? data : {},
|
|
108
|
+
body: body || '',
|
|
109
|
+
...(Object.keys(i18n).length > 0 && { i18n }),
|
|
110
|
+
};
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { data: {}, body: body || text, parseError: `frontmatter: ${e.message}` };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────── inference ────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const BRAND_KEYWORDS = [
|
|
119
|
+
// tooling
|
|
120
|
+
['Hermes', /\bhermes\b/i],
|
|
121
|
+
['Claude Code', /\bclaude[\s-]?code\b/i],
|
|
122
|
+
['Anthropic', /\b(anthropic|claude)\b/i],
|
|
123
|
+
['OpenAI', /\b(openai|chatgpt|gpt-?[0-9]|codex)\b/i],
|
|
124
|
+
['Cursor', /\bcursor\b/i],
|
|
125
|
+
['GitHub', /\b(github|\bgh\b|octokit)\b/i],
|
|
126
|
+
['Cloudflare', /\b(cloudflare|cf-|workers|wrangler|r2|d1)\b/i],
|
|
127
|
+
['Google', /\b(gemini|google\s|gws)\b/i],
|
|
128
|
+
['Apple', /\b(apple|imessage|appleнотes|findmy|macos)\b/i],
|
|
129
|
+
['Notion', /\bnotion\b/i],
|
|
130
|
+
['Linear', /\blinear\b/i],
|
|
131
|
+
['Obsidian', /\bobsidian\b/i],
|
|
132
|
+
['Modal', /\bmodal\b/i],
|
|
133
|
+
['HuggingFace', /\b(huggingface|hf-cli|hf\s+hub)\b/i],
|
|
134
|
+
['Discord', /\bdiscord\b/i],
|
|
135
|
+
['Telegram', /\btelegram\b/i],
|
|
136
|
+
['X', /\b(twitter|x\.com|x-cli|xurl)\b/i],
|
|
137
|
+
['Vercel', /\bvercel\b/i],
|
|
138
|
+
['Docker', /\bdocker\b/i],
|
|
139
|
+
['Kubernetes', /\b(kubernetes|kubectl|k8s)\b/i],
|
|
140
|
+
['NVIDIA', /\b(cuda|nvidia)\b/i],
|
|
141
|
+
['Suno', /\bsuno\b/i],
|
|
142
|
+
['Spotify', /\bspotify\b/i],
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export function inferBrand({ name, description, category, raw }) {
|
|
146
|
+
const hay = [name, description, category].filter(Boolean).join(' ');
|
|
147
|
+
const head = (raw || '').slice(0, 800);
|
|
148
|
+
const text = `${hay} ${head}`;
|
|
149
|
+
for (const [brand, re] of BRAND_KEYWORDS) {
|
|
150
|
+
if (re.test(text)) return brand;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Infer "product" — typically the skill name itself for product-shaped skills
|
|
157
|
+
* (e.g. "new-api-deployment" → "new-api"), or the bare name otherwise.
|
|
158
|
+
*/
|
|
159
|
+
export function inferProduct({ name, category }) {
|
|
160
|
+
if (!name) return undefined;
|
|
161
|
+
// Strip common suffixes that aren't product names
|
|
162
|
+
const cleaned = name
|
|
163
|
+
.replace(/-deployment$/, '')
|
|
164
|
+
.replace(/-workflow$/, '')
|
|
165
|
+
.replace(/-bot-notifications$/, '')
|
|
166
|
+
.replace(/-management$/, '');
|
|
167
|
+
return cleaned;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─────────────────────────────── markdown ─────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Extract a one-line description: prefer frontmatter.description, else first
|
|
174
|
+
* non-empty paragraph (no headings, no blank).
|
|
175
|
+
*/
|
|
176
|
+
export function deriveDescription(fmData, body) {
|
|
177
|
+
if (fmData?.description && typeof fmData.description === 'string') {
|
|
178
|
+
return fmData.description.trim();
|
|
179
|
+
}
|
|
180
|
+
for (const line of body.split(/\r?\n/)) {
|
|
181
|
+
const t = line.trim();
|
|
182
|
+
if (!t) continue;
|
|
183
|
+
if (t.startsWith('#')) continue;
|
|
184
|
+
if (t.startsWith('---')) continue;
|
|
185
|
+
return t.slice(0, 240);
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function makePreview(body, maxChars = 600) {
|
|
191
|
+
const trimmed = (body || '').trim();
|
|
192
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
193
|
+
return trimmed.slice(0, maxChars).trimEnd() + '…';
|
|
194
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skillhelper/server",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"fastify": "^5.0.0",
|
|
12
|
+
"chokidar": "^4.0.3",
|
|
13
|
+
"@skillhelper/scanner": "*"
|
|
14
|
+
}
|
|
15
|
+
}
|