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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/bin/lib/atomic-write.mjs +72 -0
  4. package/bin/lib/paths.mjs +65 -0
  5. package/bin/lib/port.mjs +23 -0
  6. package/bin/skillhelper.mjs +720 -0
  7. package/build/build-site.mjs +35 -0
  8. package/build/build-web.mjs +29 -0
  9. package/build/pack-smoke.mjs +195 -0
  10. package/build/prepare-publish.mjs +3 -0
  11. package/build/verify.mjs +157 -0
  12. package/config/sources.example.yaml +126 -0
  13. package/docs/Frontend-Spec.md +1207 -0
  14. package/docs/scan_skills_rules.md +432 -0
  15. package/package.json +95 -0
  16. package/packages/scanner/package.json +15 -0
  17. package/packages/scanner/src/adapters/claude-agents.mjs +134 -0
  18. package/packages/scanner/src/adapters/claude-plugin.mjs +260 -0
  19. package/packages/scanner/src/adapters/codex-plugin.mjs +330 -0
  20. package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
  21. package/packages/scanner/src/adapters/file-docs.mjs +158 -0
  22. package/packages/scanner/src/adapters/hermes-plugin.mjs +149 -0
  23. package/packages/scanner/src/adapters/markdown-skill.mjs +229 -0
  24. package/packages/scanner/src/adapters/mcp-config.mjs +190 -0
  25. package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
  26. package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
  27. package/packages/scanner/src/adapters/tier1-editor-skills.mjs +278 -0
  28. package/packages/scanner/src/adapters/tier2-user-skills.mjs +218 -0
  29. package/packages/scanner/src/adapters/tier3-other-skills.mjs +225 -0
  30. package/packages/scanner/src/config/editor-tiers.mjs +191 -0
  31. package/packages/scanner/src/core/atomic-write.mjs +11 -0
  32. package/packages/scanner/src/core/descriptor.mjs +143 -0
  33. package/packages/scanner/src/core/path-hash.mjs +89 -0
  34. package/packages/scanner/src/core/registry.mjs +128 -0
  35. package/packages/scanner/src/hash/path-hash.mjs +8 -0
  36. package/packages/scanner/src/icon/brand-map.mjs +268 -0
  37. package/packages/scanner/src/icon/icon-extractor.mjs +466 -0
  38. package/packages/scanner/src/index.mjs +450 -0
  39. package/packages/scanner/src/types.d.ts +144 -0
  40. package/packages/scanner/src/utils.mjs +194 -0
  41. package/packages/server/package.json +15 -0
  42. package/packages/server/src/index.mjs +1071 -0
  43. package/packages/server/src/labels.mjs +64 -0
  44. package/packages/server/src/langdetect.mjs +34 -0
  45. package/packages/server/src/llm-client.mjs +42 -0
  46. package/packages/server/src/translate-cache.mjs +132 -0
  47. package/packages/server/src/translator.mjs +53 -0
  48. package/packages/web/README.md +36 -0
  49. package/packages/web/dist/assets/index-CGt04vt0.js +304 -0
  50. package/packages/web/dist/assets/index-CLSDoa3H.css +1 -0
  51. package/packages/web/dist/brand-icon.png +0 -0
  52. package/packages/web/dist/brand-logo.png +0 -0
  53. package/packages/web/dist/favicon-16x16.png +0 -0
  54. package/packages/web/dist/favicon-192x192.png +0 -0
  55. package/packages/web/dist/favicon-32x32.png +0 -0
  56. package/packages/web/dist/favicon-512x512.png +0 -0
  57. package/packages/web/dist/favicon-source.svg +5 -0
  58. package/packages/web/dist/favicon.ico +0 -0
  59. package/packages/web/dist/favicon.svg +5 -0
  60. package/packages/web/dist/icons/hermes-128.png +0 -0
  61. package/packages/web/dist/icons/hermes-192.png +0 -0
  62. package/packages/web/dist/icons/hermes-256.png +0 -0
  63. package/packages/web/dist/icons/hermes-32.png +0 -0
  64. package/packages/web/dist/icons/hermes-512.png +0 -0
  65. package/packages/web/dist/icons/hermes-64.png +0 -0
  66. package/packages/web/dist/icons/hermes.png +0 -0
  67. package/packages/web/dist/index.html +37 -0
  68. package/packages/web/dist/robots.txt +3 -0
  69. package/packages/web/dist/site.webmanifest +24 -0
  70. package/packages/web/public/brand-icon.png +0 -0
  71. package/packages/web/public/brand-logo.png +0 -0
  72. package/packages/web/public/favicon-16x16.png +0 -0
  73. package/packages/web/public/favicon-192x192.png +0 -0
  74. package/packages/web/public/favicon-32x32.png +0 -0
  75. package/packages/web/public/favicon-512x512.png +0 -0
  76. package/packages/web/public/favicon-source.svg +5 -0
  77. package/packages/web/public/favicon.ico +0 -0
  78. package/packages/web/public/favicon.svg +5 -0
  79. package/packages/web/public/icons/hermes-128.png +0 -0
  80. package/packages/web/public/icons/hermes-192.png +0 -0
  81. package/packages/web/public/icons/hermes-256.png +0 -0
  82. package/packages/web/public/icons/hermes-32.png +0 -0
  83. package/packages/web/public/icons/hermes-512.png +0 -0
  84. package/packages/web/public/icons/hermes-64.png +0 -0
  85. package/packages/web/public/icons/hermes.png +0 -0
  86. package/packages/web/public/robots.txt +3 -0
  87. package/packages/web/public/site.webmanifest +24 -0
  88. package/scripts/install-and-sync.sh +209 -0
  89. 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
+ }